Todos los proyectos Laravel que he heredado tienen la misma capa fósil en alguna parte: un SeoTrait atornillado a los controladores, un array de configuración con títulos por defecto y un parcial de Blade lleno de condicionales que decide si og:image debe caer al logo del sitio. Todos escribimos alguna versión de eso, y todos sabíamos en el fondo que estaba sujeto con cinta adhesiva. Así que cuando Taylor Otwell anunció Laravel Head en la Laracon US 2026, mi primera reacción no fue 'por fin, meta tags más bonitas'. Fue: por fin alguien le pone nombre al problema real. Los metadatos del head nunca fueron un asunto de plantillas. Son estado en cascada —valores por defecto, reglas de grupo, valores de ruta, datos en tiempo de petición— y llevábamos años gestionando esa cascada a mano, y mal.

Esa es mi tesis, y el diseño del paquete la respalda. Laravel Head resuelve los metadatos a través de cinco capas, de menor a mayor prioridad: valores por defecto de página, metadatos de grupo de rutas, metadatos de ruta, metadatos en tiempo de ejecución y metadatos de páginas de error. El detalle que importa es que una capa superior solo sobrescribe los campos que realmente define. Tu controlador puede llamar a Head::title($post->title) y la descripción definida en la ruta sobrevive intacta. Esa semántica de fusión es exactamente lo que tu SeoService artesanal hizo mal al menos una vez: el bug en el que una página de detalle muestra de repente el título por defecto de todo el sitio porque la sobrescritura de alguien machacó el objeto entero en vez de un solo campo. Si has publicado ese bug (yo sí), ya entiendes por qué esto es un problema de resolución, no de renderizado.

La ergonomía se deriva de ese modelo en lugar de pelearse con él. Los valores por defecto del sitio viven en un service provider vía Head::defaults(), las páginas estáticas adjuntan sus metadatos directamente en la ruta con withHead(), y toda una sección de administración pasa a noindex con un único argumento robots en el grupo de rutas: sin middleware, sin husmear prefijos de URL. Los condicionales se vuelven parte de la cadena: ->when($post->is_draft, fn ($head) => $head->hiddenFromRobots()) sustituye al if-else que antes escondías en una vista de Blade. Y el impuesto de la duplicación desaparece: og:title y og:description se derivan del título y la descripción que ya definiste, las Twitter cards se derivan de esos mismos valores una vez declaras un tipo de tarjeta como TwitterCard::SummaryWithLargeImage en tus defaults, y solo sobrescribes cuando el texto para redes es genuinamente distinto.

La parte que yo calificaría de infravalorada es la historia multi-stack. Los bugs de head más desastrosos que he visto no estaban en Blade a secas: aparecían cuando el wire:navigate de Livewire se saltaba la carga completa de página y el título se quedaba obsoleto, o cuando una app con Inertia repartía la lógica del título en un componente <Head> de React que tu código PHP no podía ver. El resolver de Laravel Head tiene alcance de petición, así que la navegación de Livewire recoge los metadatos de la ruta de destino sin cableado extra. Inertia necesita más ceremonia —@head antes de <x-inertia::head />, serverHead: true en createInertiaApp() (Inertia 3.5+), y las etiquetas verdaderamente estáticas registradas mediante Head::inertiaGlobals()—, pero la recompensa es una única fuente de verdad en lugar de dos sistemas forcejeando por el mismo elemento del DOM. Los builders de JSON-LD son la misma idea aplicada a schema.org: builders tipados para product, breadcrumbs, faq y compañía, posiciones asignadas automáticamente y un atributo #[SchemaType] como vía de escape para todo lo que no venga de serie.

Ahora el contraargumento honesto, porque es bueno: esto no necesitaba ser first-party. spatie/laravel-seo existe y está bien mantenido, y cada paquete first-party que Laravel absorbe estrecha el espacio en el que prosperan los paquetes comunitarios. Hay un coste real cuando el framework bendice una respuesta: Spatie, Artesaos y media docena de autores más pequeños hicieron años de trabajo de diseño no remunerado en este terreno, y lo 'oficial' tiende a cerrar conversaciones en vez de continuarlas. Si tu proyecto es pequeño y tu configuración actual no duele, migrar solo por la insignia es puro cargo cult.

Aun así me quedo del lado del first-party, por una razón concreta: la cascada de cinco capas solo funciona si puede engancharse a las definiciones de rutas, los grupos de rutas, las páginas de error y el ciclo de vida de la petición como iguales. Un paquete comunitario llega a esas costuras mediante puntos de extensión y apaños; uno first-party consigue Route::withHead() como ciudadano nativo. Algunos problemas pertenecen genuinamente al framework porque su solución correcta atraviesa el framework, y resulta que 'cuál es el título de esta respuesta' es uno de ellos. El suelo de versiones es real, eso sí: el mínimo de PHP 8.3 y Laravel 13.17 significa que muchas apps en producción no podrán tocarlo durante un tiempo, y no pasa nada. El consejo del propio artículo es sensato: migra primero título y descripción, luego Open Graph, luego schema. Nadie necesita un pull request de 4,000 líneas titulado 'reemplazar sistema SEO'.

Una cosa pequeña que señalaría antes de que te lances de cabeza: los metadatos derivados son una comodidad de doble filo. Que og:title refleje silenciosamente tu título es genial hasta que marketing quiere un texto social distinto en doce páginas y alguien tiene que recordar qué valores son derivados y cuáles explícitos. La vieja duplicación era fea, pero al menos era visible. Mi apuesta es que el intercambio merece la pena —las sobrescrituras explícitas como Head::twitter(title: $post->social_title) mantienen legibles las excepciones—, pero es el tipo de cosa que solo juzgas de verdad a los seis meses de proyecto.

Así que esto es lo que realmente quiero saber de ti: si has construido o adoptado una capa de gestión del head —la de Spatie, tu propio trait, un montaje con <Head> de Inertia—, ¿el modelo de cinco capas encaja con cómo fluyen realmente tus metadatos, o tienes un caso que no puede expresar? El que más me interesa es el de las apps multi-tenant donde los propios 'defaults' son por tenant y viven en la base de datos. Si tienes una de esas, cuéntame abajo si la cascada de Laravel Head la aguantaría o se agrietaría bajo su peso.