Aller au contenu

Titres, descriptions, Open Graph

Temps 1 · séance 5 · ~25 min

Les slides du chapitre (PDF)PDF

  • Mettre votre nom dans le titre de chaque onglet avec APP_NAME.
  • Donner une description à chaque page publique avec <Head>.
  • Écrire les balises Open Graph d'une fiche dans le template Blade avec withViewData.
  • Vérifier ce qu'un robot reçoit, avec la vue source du site compilé.

1. Votre nom dans l'onglet

Au chapitre 1.08, vous avez mis votre nom dans la variable APP_NAME de .env, et l'onglet de la liste s'intitule « Projets - Nora Benali ». Si le vôtre affiche encore « Projets - Laravel », le kit a gardé sa valeur : remplacez-la par votre nom :

APP_NAME="Nora Benali"

Écrivez votre nom à la place de celui de l'exemple. Les guillemets sont nécessaires dès que la valeur contient une espace.

La page Vue ne lit pas APP_NAME directement. Elle lit VITE_APP_NAME, une variable que le kit définit plus bas dans .env par VITE_APP_NAME="${APP_NAME}". Vite ne transmet au JavaScript que les variables dont le nom commence par VITE_, et il les lit à son démarrage. Arrêtez composer run dev avec Ctrl+C, puis relancez-le, comme au chapitre 1.18. Le titre de la fiche du quiz devient :

<title data-inertia="">Quiz des capitales - Nora Benali</title>

2. Une description par page

La balise <meta name="description"> résume une page en une phrase. Les moteurs de recherche peuvent l'afficher sous le titre de la page dans leurs résultats. Le composant <Head> du chapitre 1.08 l'accepte entre ses balises. Dans Home.vue, remplacez <Head title="Accueil" /> par :

<Head title="Accueil">
    <meta
        head-key="description"
        name="description"
        content="Portfolio de Nora Benali, développeuse web Laravel et Vue.js à Wavre."
    />
</Head>

L'attribut head-key donne une clé à la balise. Quand vous passez d'une page à l'autre avec <Link>, Inertia remplace la balise qui porte la même clé, au lieu d'en ajouter une à chaque page visitée.

Faites de même dans projects/Index.vue, About.vue et Contact.vue, chacune avec sa phrase. Dans la fiche, projects/Show.vue, le titre et la description viennent du projet :

<Head :title="project.title">
    <meta
        head-key="description"
        name="description"
        :content="project.summary"
    />
</Head>

3. L'aperçu d'un lien partagé : Open Graph

Collez l'adresse d'un site dans LinkedIn, WhatsApp, Slack ou Discord. Une carte apparaît sous le message, avec un titre, une phrase et une image. Le site les a déclarés dans des balises Open Graph, des balises <meta property="og:…"> placées dans le <head> de la page. Trois suffisent pour une fiche de projet :

Balise Contenu pour une fiche
og:title le titre du projet
og:description le résumé du projet
og:image l'adresse complète de l'image de couverture

Un premier essai dans <Head>

L'idée naturelle consiste à ajouter ces trois balises dans le <Head> de Show.vue, à côté de la description. Avec composer run dev, le HTML de la fiche les contient bien :

$ curl -s http://127.0.0.1:8000/projets/quiz-des-capitales | grep "og:"
<meta property="og:title" content="Quiz des capitales" data-inertia>
<meta property="og:description" content="Un quiz de géographie en Vue.js avec score, minuteur et classement local." data-inertia>
<meta property="og:image" content="http://127.0.0.1:8000/storage/images/quiz-capitales.jpg" data-inertia>
<title data-inertia="">Quiz des capitales - Nora Benali</title>
<meta name="description" content="Un quiz de géographie en Vue.js avec score, minuteur et classement local." data-inertia="description">

curl est un programme qui demande une adresse et affiche la réponse brute du serveur, sans exécuter de JavaScript. Il voit ce que voit un robot.

Le même projet, compilé avec npm run build et servi sans composer run dev, comme il le sera chez OVH, donne autre chose :

$ curl -s http://127.0.0.1:8000/projets/quiz-des-capitales | grep -oE "<title[^<]*</title>|<meta property=\"og[^>]*>|<meta name=\"description[^>]*>"
<title>Nora Benali</title>

Il ne reste ni description, ni Open Graph, et le titre est générique.

Pourquoi les balises disparaissent

En développement, le plugin d'Inertia pour Vite fait un rendu côté serveur : il construit le HTML de la page Vue avant de l'envoyer, balises de <Head> comprises. En production, ce rendu demande un programme Node qui tourne en permanence sur le serveur. Un hébergement mutualisé comme celui d'OVH n'exécute que PHP. Le HTML envoyé ne contient alors que la balise <script data-page> du chapitre 1.07, et Vue construit la page dans le navigateur.

En développement, PHP et le rendu côté serveur envoient un HTML qui contient og:title, og:description, og:image, la description et le titre du projet. En ligne chez OVH, seul PHP tourne, sans rendu côté serveur ni Node : le HTML ne contient que le titre Nora Benali et la balise script data-page, ni description ni Open Graph

Les balises de <Head> n'apparaissent donc qu'après l'exécution du JavaScript. Dans le labo, la description arrive dans le <head> une seconde et demie après le chargement. Google exécute le JavaScript et la trouve. Les robots d'aperçu de Facebook, LinkedIn, Slack, WhatsApp ou Discord ne l'exécutent pas : sans correction, votre lien partagé n'a ni image ni description.

En ligne, PHP envoie un HTML qui ne contient que le titre Nora Benali et la balise script data-page. Le navigateur et Google exécutent le JavaScript : Head ajoute la description et les balises og, une seconde et demie après. Le robot d'aperçu n'exécute pas le JavaScript : il ne voit que le titre Nora Benali, ni image ni description

Une balise qu'un robot doit lire s'écrit en PHP, dans le template Blade.

4. Les balises Open Graph dans le template Blade

Le kit a un seul template Blade, resources/views/app.blade.php, qui entoure toutes les pages. Laravel l'écrit en PHP à chaque requête, avant tout JavaScript. La méthode withViewData() lui passe des données, à côté des props de la page Vue. Dans ProjectController::show(), ajoutez-la après Inertia::render(...) :

return Inertia::render('projects/Show', [
    'project' => [
        ...$project->only('title', 'slug', 'summary', 'content', 'demo_url', 'repo_url'),
        'cover_url' => $project->cover_url,
        'tags' => $project->tags->pluck('name'),
    ],
])->withViewData([
    'meta' => [
        'title' => $project->title,
        'description' => $project->summary,
        'image' => $project->cover_url,
    ],
]);

ProjectController::show() envoie les props à la page Vue projects/Show, qui s'affiche dans le navigateur après le JavaScript, et passe meta par withViewData au template app.blade.php. Le template, écrit en PHP à chaque requête, contient og:title, og:description, og:image et le title avant tout JavaScript : le robot d'aperçu y lit le titre, le résumé et l'image

Les props partent vers la page Vue, et meta reste côté serveur, pour le template. Dans app.blade.php, ajoutez ces lignes juste avant @fonts :

@isset($meta)
    <meta property="og:title" content="{{ $meta['title'] }}">
    <meta property="og:description" content="{{ $meta['description'] }}">
    @if ($meta['image'])
        <meta property="og:image" content="{{ $meta['image'] }}">
    @endif
@endisset

@isset($meta) n'écrit les balises que pour une page qui a passé meta, ici la fiche. @if ($meta['image']) saute l'image d'un projet qui n'en a pas. {{ }} échappe le texte, comme en 5XCOS : un guillemet dans un résumé ne casse pas la balise.

Le titre de l'onglet aussi doit exister sans JavaScript. Dans le même fichier, remplacez la balise <title> qui se trouve dans <x-inertia::head> :

<x-inertia::head>
    <title>{{ isset($meta) ? $meta['title'].' - ' : '' }}{{ config('app.name', 'Laravel') }}</title>
</x-inertia::head>

Retirez enfin les trois balises og: du <Head> de Show.vue, si vous les y aviez mises. En développement, elles seraient écrites deux fois. Le site compilé et servi sans composer run dev répond maintenant, en ne gardant que ces quatre balises de la source (dans le fichier, les préchargements de polices s'intercalent entre les og: et le <title>) :

<meta property="og:title" content="Quiz des capitales">
<meta property="og:description" content="Un quiz de géographie en Vue.js avec score, minuteur et classement local.">
<meta property="og:image" content="http://127.0.0.1:8000/storage/images/quiz-capitales.jpg">
<title>Quiz des capitales - Nora Benali</title>

Regardez l'adresse de l'image. Elle commence par la valeur de APP_URL, quelle que soit l'adresse ouverte dans le navigateur. Un robot ne peut charger que la vraie adresse publique de l'image : en ligne, APP_URL doit être celle de votre site, ce que règle le chapitre 1.22.

5. Le favicon

Le favicon est la petite icône de l'onglet et des favoris. Le kit en livre trois, avec le logo de Laravel, liés dans app.blade.php :

<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">

Remplacez les trois fichiers du dossier public/ par les vôtres, en gardant leurs noms. Un générateur de favicon en ligne produit les trois à partir d'une image. Les initiales de votre nom dans la couleur de votre planche suffisent.

6. Vérifier

Vérifiez ce que reçoit un robot, pas ce qu'affiche votre navigateur.

  1. Compilez le site avec npm run build, puis arrêtez composer run dev. Si le dossier public/hot existe encore, supprimez-le : tant qu'il est là, Laravel attend Vite.
  2. Ouvrez une fiche sur portfolio.test, puis sa vue source avec Ctrl+U. Cherchez og: : les trois balises sont là, sans data-inertia.

Vue source de la fiche Quiz des capitales : trois balises meta og:title, og:description et og:image, juste au-dessus des polices

L'inspecteur des outils de développement ne convient pas. Il montre la page après l'exécution du JavaScript, et il afficherait les balises de <Head> même si un robot ne les voit pas. Le site en développement ne convient pas non plus, puisque son rendu côté serveur cache le problème.

Une fois le site en ligne, un outil d'aperçu montre la carte telle qu'elle apparaîtra : le débogueur de partage de Facebook, le Post Inspector de LinkedIn ou opengraph.xyz. Ces outils chargent la page depuis Internet. Ils ne voient ni portfolio.test ni votre machine.

7. Un test qui lit le HTML

Un test Pest reçoit le HTML que Laravel envoie, sans rendu côté serveur et sans JavaScript, exactement comme un robot. Ajoutez ce test dans tests/Feature/PublicPagesTest.php :

test('a project page has Open Graph tags in the HTML', function () {
    Project::factory()->create([
        'title' => 'Météo Wavre',
        'slug' => 'meteo-wavre',
        'summary' => 'La météo heure par heure.',
        'cover_path' => 'images/meteo-wavre.jpg',
    ]);

    $this->get('/projets/meteo-wavre')
        ->assertSee('<meta property="og:title" content="Météo Wavre">', false)
        ->assertSee('<meta property="og:description" content="La météo heure par heure.">', false)
        ->assertSee('/storage/images/meteo-wavre.jpg', false);
});

Le test Pest appelle $this->get('/projets/meteo-wavre') et reçoit le même HTML que le robot d'aperçu, sans JavaScript. assertSee y trouve la balise og:title, la balise og:description et /storage/images/meteo-wavre.jpg, écrites par le template Blade. Head n'est pas exécuté

Le second argument de assertSee, false, demande de chercher le texte tel quel, sans échapper ses guillemets et ses chevrons. Dans le labo, le fichier contient aussi un test de l'accueil et le test des mentions légales du chapitre 1.21 :

php artisan test tests/Feature/PublicPagesTest.php
   PASS  Tests\Feature\PublicPagesTest
  ✓ the home page shows three published projects
  ✓ a project page has Open Graph tags in the HTML
  ✓ the legal notice page exists

  Tests:    3 passed (21 assertions)

Le référencement ne s'arrête pas au titre, à la description et à Open Graph. Le plan du site pour les moteurs, la vitesse de chargement ou les données structurées viendront avec le projet en binôme.

8. Pratique : titres et aperçus (10 min)

Temps annoncé : 10 minutes.

  • APP_NAME porte votre nom, et chaque onglet affiche « titre de la page - votre nom ».
  • Chaque page publique a une description avec head-key="description".
  • ProjectController::show() passe meta avec withViewData(), et app.blade.php écrit les trois balises og:.
  • Après npm run build, sans composer run dev, la vue source d'une fiche contient og:title, og:description et og:image.
  • Les trois favicons du kit sont remplacés par les vôtres.
  • Le test Open Graph est vert.

Ce que votre portfolio contient maintenant

Un titre et une description pour chaque page publique, et des balises Open Graph écrites par le serveur sur chaque fiche de projet. Un lien partagé vers une fiche montrera son titre, son résumé et son image dès que le site sera en ligne. L'onglet porte votre nom et votre favicon.

À retenir

  • APP_NAME dans .env donne le suffixe des titres, et Vite le lit au démarrage par VITE_APP_NAME.
  • <Head> donne à chaque page son titre et sa description, et head-key évite d'empiler les balises.
  • Sans rendu côté serveur, ce qu'écrit <Head> n'existe qu'après le JavaScript, et un robot d'aperçu ne le voit pas.
  • withViewData(['meta' => …]) passe les données au template app.blade.php, qui écrit les balises Open Graph en PHP.
  • La vérification se fait dans la vue source du site compilé, sans composer run dev, puis avec un outil d'aperçu une fois en ligne.