Chaque codebase Laravel dont j'ai hérité contient la même couche fossile quelque part : un SeoTrait vissé sur les contrôleurs, un tableau de config avec des titres par défaut, et un partial Blade bourré de conditions pour décider si og:image doit retomber sur le logo du site. On en a tous écrit une version, et on savait tous, sans le dire, que ça tenait avec du scotch. Alors quand Taylor Otwell a annoncé Laravel Head à la Laracon US 2026, ma première réaction n'a pas été « enfin, des meta tags plus jolis ». C'était : enfin, quelqu'un nomme le vrai problème. Les métadonnées du head n'ont jamais été une affaire de templating. C'est de l'état en cascade — valeurs par défaut, règles de groupe, valeurs de route, données au moment de la requête — et on gérait cette cascade à la main, et mal.

C'est ma thèse, et le design du package la confirme. Laravel Head résout les métadonnées à travers cinq couches, de la priorité la plus basse à la plus haute : défauts de page, métadonnées de groupe de routes, métadonnées de route, métadonnées runtime, et métadonnées de pages d'erreur. Le détail qui compte, c'est qu'une couche supérieure n'écrase que les champs qu'elle définit réellement. Ton contrôleur peut appeler Head::title($post->title) et la description définie sur la route survit intacte. Cette sémantique de merge est précisément ce que ton SeoService fait maison a raté au moins une fois — le bug où une page de détail affiche soudain le titre par défaut du site parce que l'override de quelqu'un a écrasé l'objet entier au lieu d'un seul champ. Si tu as livré ce bug (moi oui), tu comprends déjà pourquoi c'est un problème de résolution, pas un problème de rendu.

L'ergonomie découle de ce modèle au lieu de le combattre. Les défauts globaux vivent dans un service provider via Head::defaults(), les pages statiques attachent leurs métadonnées directement sur la route avec withHead(), et toute une section admin passe en noindex avec un seul argument robots sur le groupe de routes — pas de middleware, pas de sniffing de préfixe d'URL. Les conditions deviennent partie de la chaîne : ->when($post->is_draft, fn ($head) => $head->hiddenFromRobots()) remplace le if-else que tu planquais dans une vue Blade. Et la taxe de duplication disparaît : og:title et og:description dérivent du title et de la description que tu as déjà définis, les Twitter cards dérivent des mêmes valeurs une fois que tu as déclaré un type de card comme TwitterCard::SummaryWithLargeImage dans tes défauts, et tu n'overrides que quand le texte social diffère vraiment.

La partie que je trouve sous-estimée, c'est l'histoire cross-stack. Les bugs de head les plus crades que j'ai vus n'étaient pas dans du Blade pur — ils apparaissaient quand wire:navigate de Livewire sautait le chargement complet de la page et que le titre restait périmé, ou quand une app Inertia éclatait la logique de titre dans un composant React <Head> invisible pour ton code PHP. Le résolveur de Laravel Head est scoped à la requête, donc la navigation Livewire récupère les métadonnées de la route de destination sans le moindre câblage supplémentaire. Inertia demande plus de cérémonie — @head avant <x-inertia::head />, serverHead: true dans createInertiaApp() (Inertia 3.5+), et les tags vraiment statiques enregistrés via Head::inertiaGlobals() — mais la récompense, c'est une seule source de vérité au lieu de deux systèmes qui se battent pour le même élément du DOM. Les builders JSON-LD, c'est la même idée appliquée à schema.org : des builders typés pour product, breadcrumbs, faq et compagnie, les positions assignées automatiquement, et un attribut #[SchemaType] comme porte de sortie pour tout ce qui n'est pas intégré.

Maintenant le contre-argument honnête, parce qu'il est bon : rien n'obligeait à faire ça en first-party. spatie/laravel-seo existe et est bien maintenu, et chaque package first-party que Laravel absorbe rétrécit l'espace où les packages communautaires prospèrent. Il y a un vrai coût quand le framework consacre une seule réponse — Spatie, Artesaos et une demi-douzaine d'auteurs plus petits ont fait des années de travail de design non rémunéré sur ce terrain, et « officiel » a tendance à clore les conversations plutôt qu'à les prolonger. Si ton projet est petit et que ton setup actuel ne fait pas mal, migrer juste pour le badge, c'est du cargo cult.

J'atterris quand même du côté du first-party, pour une raison précise : la cascade en cinq couches ne fonctionne que si elle peut s'accrocher aux définitions de routes, aux groupes de routes, aux pages d'erreur et au cycle de vie de la requête d'égal à égal. Un package communautaire atteint ces coutures par des points d'extension et des contournements ; un package first-party obtient Route::withHead() comme citoyen natif. Certains problèmes appartiennent vraiment au framework parce que leur solution correcte traverse le framework de part en part, et « quel est le titre de cette réponse » s'avère en faire partie. Le plancher de versions est réel, cela dit — PHP 8.3 et Laravel 13.17 minimum, ça veut dire que pas mal d'apps en production ne pourront pas y toucher avant un moment, et c'est très bien. Le conseil de l'article lui-même est sain : migre d'abord title et description, puis l'Open Graph, puis le schema. Personne n'a besoin d'une pull request de 4 000 lignes « remplacer le système SEO ».

Un petit truc que je signalerais avant que tu te lances à fond : les métadonnées dérivées sont une commodité à double tranchant. Quand og:title reflète silencieusement ton title, c'est génial jusqu'à ce que le marketing veuille un texte social différent sur douze pages et que quelqu'un doive se rappeler quelles valeurs sont dérivées et lesquelles sont explicites. L'ancienne duplication était moche, mais au moins elle était visible. Mon pari, c'est que le compromis en vaut la peine — des overrides explicites comme Head::twitter(title: $post->social_title) gardent les exceptions lisibles — mais c'est le genre de chose qu'on ne juge vraiment qu'au bout de six mois de projet.

Alors voilà ce que je veux vraiment savoir de ta part : si tu as construit ou adopté une couche de gestion du head — celle de Spatie, ton propre trait, un setup Inertia avec <Head> — est-ce que le modèle en cinq couches colle à la façon dont tes métadonnées circulent réellement, ou est-ce que tu as un cas qu'il ne peut pas exprimer ? Celui qui m'intéresse le plus, ce sont les apps multi-tenant où les « défauts » eux-mêmes sont par tenant et vivent en base de données. Si tu en as une, dis-moi en commentaire si la cascade de Laravel Head la porterait ou craquerait sous le poids.