Aller au contenu

Naviguer : Link, Wayfinder, layouts, <Head>, 404

Temps 1 · séance 2 · ~50 min

Les slides du chapitre (PDF)

  • Lier les pages avec <Link>, sans recharger la page entière.
  • Appeler une route nommée de Laravel depuis Vue avec Wayfinder.
  • Partager un layout entre toutes les pages publiques.
  • Donner un titre à chaque page avec <Head>.
  • Afficher une page 404 dans votre site plutôt que celle du framework.

Ouvrez l'onglet Réseau des outils de développement, puis cliquez sur « Voir le projet » dans votre liste. Le navigateur recharge tout : le HTML, la feuille de style, les scripts de Vue et du kit. Une balise <a> demande une page complète, comme sur n'importe quel site.

Inertia fournit un composant qui fait mieux, <Link>. Il s'importe de @inertiajs/vue3 et s'écrit comme une balise a, avec href. Dans le HTML, il produit d'ailleurs une vraie balise <a href="/projets">.

La différence se voit au clic. <Link> intercepte le clic et envoie lui-même une requête avec l'en-tête X-Inertia: true. Laravel reconnaît cet en-tête et renvoie seulement le JSON de la page, sans HTML. Vue remplace alors le composant de la page et met à jour l'adresse dans la barre du navigateur.

Avec <Link>, l'onglet Réseau ne montre plus de document HTML. La requête vers la page demandée, projets/quiz-des-capitales par exemple, est de type xhr, avec le statut 200. Elle est suivie du fichier JavaScript de la page s'il n'est pas encore chargé et de ses images. La feuille de style app.css est seulement revérifiée, statut 304, inchangée. Les scripts de Vue et du kit ne sont pas redemandés. La capture ci-dessous montre un clic sur « Projets » dans le menu que vous ajoutez à la section 3 : huit lignes, dont la requête projets, sélectionnée. Voici la réponse du serveur à une requête de <Link> vers la fiche du quiz, coupée :

Onglet Réseau après un clic sur Projets : huit requêtes, dont projets de type xhr, statut 200, avec Content-Type: application/json et X-Inertia: true dans les en-têtes de réponse

HTTP/1.1 200 OK
X-Inertia: true
Content-Type: application/json
Vary: X-Inertia

{"component":"projects\/Show","props":{"errors":{},"name":"Laravel","auth":{"user":null},"sidebarOpen":true,"project":{"title":"Quiz des capitales",…}},…}

Le Content-Type dit application/json : le serveur n'a envoyé que le nom du composant et ses props. La feuille de style et les scripts déjà chargés restent en place, et le passage d'une page à l'autre est plus rapide.

Un clic sur une balise a recharge tout, le HTML, la feuille de style et les scripts ; un clic sur Link envoie X-Inertia: true et reçoit le JSON de la page, la feuille de style et les scripts restent en place et seul le composant de la page change

2. Wayfinder : les routes de Laravel dans Vue

Le lien de la liste s'écrit encore ainsi, avec l'adresse en dur :

<a :href="`/projets/${project.slug}`">Voir le projet</a>

L'adresse /projets est écrite deux fois, dans routes/web.php et dans la page. Si vous la changez dans routes/web.php, le lien de la page mène à une 404, sans aucun message d'erreur au moment de la modification.

Les fichiers générés

Wayfinder, installé avec le kit, lit vos routes PHP et écrit une fonction pour chaque route nommée, dans un fichier TypeScript que vos pages JavaScript importent. Les fichiers générés sont rangés dans trois dossiers de resources/js/.

Dossier Ce qu'il contient
routes/ une fonction par nom de route, par exemple routes/projects/index.ts pour projects.index et projects.show, et routes/index.ts pour home, about et contact
actions/ une fonction par méthode de contrôleur, par exemple actions/App/Http/Controllers/ProjectController.ts
wayfinder/ les fonctions communes aux deux autres dossiers

Voici la fonction show générée dans resources/js/routes/projects/index.ts pour la route projects.show :

export const show = (args: { project: string | { slug: string } } | [project: string | { slug: string } ] | string | { slug: string }, options?: RouteQueryOptions): RouteDefinition<'get'> => ({
    url: show.url(args, options),
    method: 'get',
})

show.definition = {
    methods: ["get","head"],
    url: '/projets/{project}',
} satisfies RouteDefinition<["get","head"]>

Ce fichier est en TypeScript, comme le reste du kit, et vous l'importez sans problème depuis une page écrite en JavaScript. Les types de args disent ce que la fonction accepte : une chaîne, un objet { project: … }, un tableau, ou un objet qui possède une clé slug. Le projet reçu en prop a cette clé, donc show(project) fonctionne directement. La fonction renvoie un objet avec l'adresse et la méthode HTTP, que <Link> sait lire.

Voici resources/js/pages/projects/Index.vue, avec les deux changements :

<script setup>
import { Head, Link } from '@inertiajs/vue3';
import { show } from '@/routes/projects';

defineProps({
    projects: Array,
});
</script>

<template>
    <Head title="Projets" />

    <h1>Mes projets</h1>

    <ul>
        <li v-for="project in projects" :key="project.slug">
            <img v-if="project.cover_url" :src="project.cover_url" alt="" width="300" />
            <h2>{{ project.title }}</h2>
            <p>{{ project.summary }}</p>
            <Link :href="show(project)">Voir le projet</Link>
        </li>
    </ul>
</template>

@/ est un raccourci du kit pour le dossier resources/js/. import { show } from '@/routes/projects' importe donc la fonction du fichier ci-dessus. La balise <Head>, importée sur la même ligne, attend la section 4.

Faites l'essai pendant que composer run dev tourne. Dans les deux routes de routes/web.php, remplacez projets par realisations sans toucher aux noms des routes. Chaque <Link> mène à la nouvelle adresse, sans qu'aucune page soit modifiée. Remettez ensuite projets.

Quand les fichiers sont-ils générés ?

composer run dev lance plusieurs processus, dont Vite. Au démarrage, le plugin Wayfinder de Vite génère les fichiers, et le terminal affiche ces lignes parmi les autres :

  vite │ [vite+] (client) info: Types generated for actions, routes, form variants
  vite │   Plugin: @laravel/vite-plugin-wayfinder

Ensuite, le plugin surveille routes/web.php. Chaque fois que vous enregistrez le fichier, il régénère les fonctions, et la même ligne « Types generated » revient. Une seconde plus tard, la nouvelle route est importable. npm run build, qui compile le site pour la mise en ligne, régénère aussi les fichiers.

Les trois dossiers figurent dans le .gitignore du kit. Après un git clone, ils n'existent pas tant que composer run dev n'a pas tourné. Un import qui échoue sur @/routes/… signifie en général que Vite n'a pas encore tourné.

Astuce

Pour générer les fichiers sans Vite, lancez php artisan wayfinder:generate --with-form. Sans --with-form, la commande oublie les fonctions .form() dont les formulaires du kit ont besoin, comme store.form() dans la page de connexion.

Wayfinder propose deux façons d'importer. Par nom de route, import { show } from '@/routes/projects', comme ci-dessus. Par contrôleur, import ProjectController from '@/actions/App/Http/Controllers/ProjectController', comme le fait la page de profil du kit. La navigation publique utilise les noms de routes, et les formulaires du chapitre 1.12 utiliseront les contrôleurs.

3. Un layout pour la partie publique

Vos pages publiques ont besoin d'un en-tête avec votre nom et un menu, et d'un pied de page. Un layout est un composant qui entoure chaque page avec ces éléments communs.

Deux pages de plus

Le menu mène aussi à l'à-propos et au contact. Ces deux pages n'ont pas besoin de données, donc pas de contrôleur à écrire. Ajoutez deux routes dans routes/web.php :

Route::inertia('a-propos', 'About')->name('about');
Route::inertia('contact', 'Contact')->name('contact');

Route::inertia() affiche une page sans que vous écriviez de contrôleur. Inertia fournit le sien, Inertia\Controller. Le kit l'utilisait pour sa page Welcome. Créez resources/js/pages/About.vue :

<script setup>
import { Head } from '@inertiajs/vue3';
</script>

<template>
    <Head title="À propos" />

    <h1>À propos</h1>
    <p>Étudiante en développement web à l'IFOSUP Wavre.</p>
</template>

Remplacez la phrase par le texte de votre devoir : les informations sur vous que vous avez accepté de publier, en mode CV ou en vitrine. Contact.vue a la même forme, avec le titre « Contact » et rien d'autre. Le formulaire arrive au chapitre 1.20.

PublicLayout.vue

Créez resources/js/layouts/PublicLayout.vue :

<script setup>
import { Link } from '@inertiajs/vue3';
import { about, contact, home } from '@/routes';
import { index as projectsIndex } from '@/routes/projects';
</script>

<template>
    <div class="mx-auto max-w-4xl px-4">
        <header class="flex items-center justify-between py-6">
            <Link :href="home()" class="font-semibold">Nora Benali</Link>
            <nav class="flex gap-4">
                <Link :href="projectsIndex()">Projets</Link>
                <Link :href="about()">À propos</Link>
                <Link :href="contact()">Contact</Link>
            </nav>
        </header>

        <main>
            <slot />
        </main>

        <footer class="py-6 text-sm">© Nora Benali</footer>
    </div>
</template>

Écrivez votre nom à la place de celui de l'exemple, dans l'en-tête et dans le pied de page.

La balise <slot /> marque l'endroit où la page s'affiche. Inertia place le composant de la page courante à cet endroit, et le reste du layout l'entoure.

index as projectsIndex renomme la fonction au moment de l'import. Chaque fichier de routes exporte des noms courts, comme index, show ou store, et plusieurs fichiers finiront par exporter un index. Le renommage évite la collision.

Les quelques classes Tailwind centrent le contenu et alignent le menu. Le reste du style attend votre direction artistique, au chapitre 1.18.

Le résolveur de app.ts

Au chapitre 1.07, le cas default du résolveur de resources/js/app.ts renvoyait null. Faites-lui renvoyer votre layout, après l'avoir importé en tête du fichier :

import PublicLayout from '@/layouts/PublicLayout.vue';
// …
layout: (name) => {
    switch (true) {
        case name.startsWith('auth/'):
            return AuthLayout;
        case name.startsWith('settings/'):
            return [AppLayout, SettingsLayout];
        case name === 'Dashboard' || name.startsWith('admin/'):
            return AppLayout;
        default:
            return PublicLayout;
    }
},

Toutes les pages qui ne sont ni de connexion, ni de réglages, ni d'administration reçoivent maintenant PublicLayout : Home, About, Contact, la liste et la fiche. Une page publique ajoutée plus tard le reçoit aussi, sans toucher à app.ts. Les pages de connexion, de réglages et d'administration gardent leur layout.

Une autre écriture existe : laisser return null dans le cas default, et déclarer le layout dans chaque page publique.

import PublicLayout from '@/layouts/PublicLayout.vue';
defineOptions({ layout: PublicLayout });

defineOptions() règle des options du composant depuis <script setup>. Le layout déclaré par la page l'emporte sur le résolveur. Cette écriture se répète dans chaque page, et une page qui l'oublie s'affiche sans menu. defineOptions({ layout: null }) ne retire pas le layout : Inertia prend null pour une absence de choix et retombe sur le résolveur. Seul le résolveur peut renvoyer « pas de layout ».

Quand vous passez d'une page publique à une autre avec <Link>, le layout reste en place. Seul le contenu du <slot /> change.

La fiche du projet Quiz des capitales avec l'en-tête du layout public : le nom de l'auteur à gauche, le menu Projets, À propos, Contact à droite

4. Un titre par page avec <Head>

Le composant <Head>, importé de @inertiajs/vue3, modifie la balise <head> du document depuis une page. Son attribut title donne le titre de l'onglet. About.vue, Contact.vue et projects/Index.vue l'importent déjà. Dans Home.vue et projects/Show.vue, ajoutez d'abord import { Head } from '@inertiajs/vue3'; dans le <script setup>, puis la balise dans le template. Voici projects/Show.vue complet :

<script setup>
import { Head } from '@inertiajs/vue3';

defineProps({
    project: Object,
});
</script>

<template>
    <Head :title="project.title" />

    <article>
        <h1>{{ project.title }}</h1>
        <p>{{ project.summary }}</p>
        <img v-if="project.cover_url" :src="project.cover_url" alt="" width="600" />
        <ul>
            <li v-for="tag in project.tags" :key="tag">{{ tag }}</li>
        </ul>
        <div v-html="project.content"></div>
        <a v-if="project.demo_url" :href="project.demo_url">Démo</a>
        <a v-if="project.repo_url" :href="project.repo_url">Code</a>
    </article>
</template>

La fiche prend le titre du projet, calculé à partir de la prop. Home.vue reçoit <Head title="Accueil" />, About.vue « À propos », Contact.vue « Contact », et projects/Index.vue « Projets ».

Cliquez sur « Projets » : l'onglet s'intitule « Projets - Laravel ». Le suffixe vient de cette ligne de resources/js/app.ts :

title: (title) => (title ? `${title} - ${appName}` : appName),

appName lit VITE_APP_NAME, que .env remplit avec VITE_APP_NAME="${APP_NAME}". Remplacez dès maintenant APP_NAME=Laravel par votre nom dans .env, entre guillemets s'il contient une espace : APP_NAME="Nora Benali". Si l'onglet affiche encore « Laravel », arrêtez et relancez composer run dev : Vite lit .env au démarrage. L'onglet s'intitule alors « Projets - Nora Benali ». Les descriptions des pages viendront au chapitre 1.19.

5. Une page 404 dans votre site

Ouvrez portfolio.test/projets/inconnu. Laravel répond par une 404, avec la page d'erreur du framework, intitulée « Not Found ». Elle n'a ni votre menu ni aucun lien vers votre site. Le visiteur égaré n'a plus qu'à repartir.

Inertia 3 permet de rendre une page Vue à la place. Dans app/Providers/AppServiceProvider.php, ajoutez ces lignes à la fin de la méthode boot() :

use Inertia\ExceptionResponse;
use Inertia\Inertia;

Inertia::handleExceptionsUsing(function (ExceptionResponse $response) {
    if (in_array($response->statusCode(), [403, 404])) {
        return $response->render('ErrorPage', [
            'status' => $response->statusCode(),
        ])->withSharedData();
    }

    return null;
});

Les deux lignes use vont en tête du fichier, avec les autres. handleExceptionsUsing() reçoit une fonction que Laravel appelle à chaque erreur. Pour une 404, ou une 403 « accès refusé » qui servira plus tard, elle rend la page ErrorPage avec le code en prop.

withSharedData() ajoute les props partagées du kit. Une 404 naît avant le passage dans le middleware d'Inertia, et la page n'aurait sinon ni auth ni name. Pour toute autre erreur, return null laisse Laravel faire comme avant. En développement, une erreur 500 garde ainsi sa page de débogage, avec le message et la ligne fautive.

Créez resources/js/pages/ErrorPage.vue :

<script setup>
import { Head, Link } from '@inertiajs/vue3';
import { computed } from 'vue';
import { home } from '@/routes';

const props = defineProps({
    status: Number,
});

const title = computed(() =>
    props.status === 404 ? 'Page introuvable' : 'Accès refusé',
);

const message = computed(() =>
    props.status === 404
        ? "Cette page n'existe pas ou n'est plus en ligne."
        : "Vous n'avez pas accès à cette page.",
);
</script>

<template>
    <Head :title="title" />

    <h1>{{ status }} · {{ title }}</h1>
    <p>{{ message }}</p>
    <Link :href="home()">Retour à l'accueil</Link>
</template>

defineProps renvoie les props, rangées ici dans la constante props pour être lues dans le script. computed() calcule le titre et le message à partir du code d'erreur, et les recalcule si la prop change.

Rouvrez l'adresse inconnue. Le serveur répond toujours avec le statut 404, mais avec votre page. Voici la réponse, coupée :

HTTP/1.1 404 Not Found
Host: portfolio.test
…
Content-Type: text/html; charset=utf-8
…
<script data-page="app" type="application/json">{"component":"ErrorPage","props":{"errors":{},"name":…,"auth":{"user":null},"sidebarOpen":true,"status":404…

L'onglet s'intitule « Page introuvable - Laravel ». La page reçoit PublicLayout par le cas default du résolveur, avec le menu pour repartir. Une adresse sans aucune route, comme /nimporte-quoi, affiche la même page.

Page 404 du portfolio dans le layout public : 404 · Page introuvable, une phrase et un lien Retour à l'accueil

6. La convention d'adresses

Le visiteur lit l'adresse, le développeur lit le code. Les segments fixes des adresses sont donc en français, et tout ce qui est dans le code reste en anglais : noms de routes, contrôleurs, pages, props.

Adresse Nom de la route Page
/ home Home
/projets projects.index projects/Index
/projets/{project:slug} projects.show projects/Show
/a-propos about About
/contact contact Contact

php artisan route:list le confirme, ici coupé aux routes publiques :

 GET|HEAD / .. home › routes/web.php:…
 …
 GET|HEAD a-propos .. about › Inertia\Controller › Controller
 …
 GET|HEAD contact .. contact › Inertia\Controller › Controller
 …
 GET|HEAD projets .. projects.index › ProjectController@index
 GET|HEAD projets/{project:slug} .. projects.show › ProjectController@show

Les pages Vue n'écrivent jamais une adresse : elles appellent home(), about() ou show(project). L'adresse n'existe qu'à un endroit, routes/web.php.

7. Pratique : la navigation complète (25 min)

Reliez toutes les pages publiques de votre portfolio.

  1. Les routes about et contact, et les pages About.vue, avec le texte de votre devoir, et Contact.vue, vide pour l'instant.
  2. PublicLayout.vue avec votre nom et le menu Projets, À propos, Contact, tous en <Link> vers des fonctions de Wayfinder.
  3. Le résolveur de app.ts qui donne PublicLayout par défaut.
  4. Dans la liste, le lien vers la fiche en <Link :href="show(project)">.
  5. Un <Head> dans chaque page.
  6. Inertia::handleExceptionsUsing() et ErrorPage.vue.

Vérifiez dans le navigateur.

  • Depuis l'accueil, vous atteignez les projets, une fiche, l'à-propos et le contact, puis vous revenez à l'accueil par votre nom, sans jamais taper d'adresse.
  • Dans l'onglet Réseau, un clic sur « Projets » ne recharge pas la page : la requête projets est de type xhr, avec l'en-tête X-Inertia, et aucun document HTML n'est redemandé.
  • Chaque onglet porte le titre de sa page.
  • /projets/inconnu et /nimporte-quoi affichent votre page 404, dans votre layout.
  • Votre code Vue ne contient aucune adresse écrite en dur.

Ce que votre portfolio contient maintenant

Cinq pages publiques reliées par un menu : l'accueil, la liste des projets, une fiche par projet, l'à-propos avec votre texte et un contact encore vide. Un layout commun, un titre par onglet et une page 404 dans votre site. Les adresses sont en français, et elles ne sont écrites qu'une fois, dans routes/web.php.

À retenir

  • <Link> produit une balise a, mais il demande seulement le JSON de la page avec l'en-tête X-Inertia, sans recharger le reste.
  • Wayfinder génère dans resources/js/routes/ une fonction par route nommée, régénérée par Vite à chaque modification de routes/web.php.
  • show(project) construit l'adresse de la fiche à partir de l'objet reçu en prop, grâce à sa clé slug.
  • Le cas default du résolveur de app.ts donne PublicLayout à toutes les pages publiques, et <slot /> marque la place de la page.
  • <Head title> donne son titre à chaque onglet, et Inertia::handleExceptionsUsing() rend la 404 avec votre page ErrorPage.vue.