Add Head for Blade and Inertia metadata management

Merged
BI
13h ago
+13082-2

This adds hypervel/head, a port of Laravel Head for managing document titles, metadata, canonical URLs, social cards, links, and structured data from PHP. It supports Blade and optional Inertia integration, including SSR.

The fluent builders, route macros, named arguments, schema APIs, and eager configuration methods follow upstream. The main adaptation is state ownership: Hypervel serves concurrent requests in long-lived workers, so metadata for one page must never become another request's defaults.

Request state and runtime configuration

Page data and error status are coroutine-scoped. Copying a coroutine context copies its Head data and schema objects, so child work can change its metadata without changing the parent. Mutable builders and schema objects resolve as fresh instances; managers, renderers, registries, and factories reuse worker-safe services.

Three optional hooks let applications configure request-dependent metadata during provider boot:

  • Head::resolveDefaultsUsing() builds defaults from the current request or application context.
  • Head::resolveErrorsUsing() builds error metadata from the actual response status.
  • Head::resolveCanonicalOriginUsing() supplies the preferred origin for automatic and relative canonical URLs, useful when a site has aliases or custom domains.

These hooks preserve the existing eager defaults(), errors(), and inertiaGlobals() APIs. Runtime values are resolved afresh, with precedence from defaults through route groups, route metadata, page data, and error metadata. Head does not fetch settings or add database queries. Applications can reuse settings they have already loaded.

flush() clears the current coroutine's page data and status when generating independent pages sequentially. Boot configuration remains intact. Public configuration methods explain their worker lifetime, and the guide uses team settings to show safe runtime configuration.

Blade and Inertia

Blade receives the @head directive. Inertia integration is enabled only when its provider is registered; having its classes available does not enable it. The package declares its own dependencies and discovery metadata for standalone installation.

Head remains a lazy shared prop, preserving partial reload behavior. It is registered before global middleware so early error responses also receive metadata. Registered Inertia provider subclasses enable the integration too. Initial documents and subsequent Inertia visits use stable ownership keys. The SSR decorator removes duplicate page-managed tags while preserving other SSR tags and the rendered body. It creates a new immutable response instead of modifying Hypervel's readonly SSR result, and forwards request configuration, disabling, path exclusions, and health checks to the wrapped gateway.

There is no Inertia JavaScript protocol change. The guide covers serverHead configuration in both client and SSR entry points, custom prop names, static globals, and moving overlapping client metadata into Head. Livewire integration is intentionally omitted; Blade and Inertia are the supported rendering paths.

Rendering fixes

  • Canonical normalization changes the path without altering query strings or fragments. Explicit absolute URLs, including uppercase schemes and protocol-relative URLs, retain their origin. Automatic URLs preserve the request's base path, and rendering outside HTTP uses the application URL.
  • Open Graph tracks fallback images, videos, and audio independently. Defining one media type no longer causes defaults of another type to acquire page-level precedence.
  • Literal "0" titles and descriptions survive rendering and array conversion, including Open Graph and Twitter fallbacks.
  • A later manifest replaces the earlier manifest, including during Inertia navigation. Other repeatable links retain their existing behavior.
  • Titles and metadata named title have separate Inertia ownership, so both survive deduplication.
  • Directly constructed builders use the registered schema factory and therefore see custom schema types.
  • Distinct meta name and property attributes keep separate Inertia identities, while normal generic tags retain their ability to override dedicated builders.
  • Pagination replaces the complete previous/next relation set, removing stale links, and accepts ordinary or cursor paginators.
  • Feed route metadata accepts bare URLs, URL lists, titled maps, and structured definitions with optional titles. Invalid resource-hint maps produce the existing route input error instead of rendering the wrong URL.
  • Schema setters preserve named and explicit null values. Inferred schema types retain their concrete class names, numeric FAQ and breadcrumb labels work, and validation descends into nested schema objects.
  • Registering the same custom tag builder twice is harmless; conflicting builders are still rejected.

The port also includes the canonical-to-Open-Graph URL fallback and Event/Place helpers proposed upstream. An unset og:url follows the resolved canonical when Open Graph metadata is present, with explicit URLs taking precedence. Event builders gain location, image, performer, status and attendance-mode helpers, plus a Place builder and schema.org enums. Their typed conventions follow the existing schema builders; set() remains available for raw schema values.

The Open Graph fix replaces its single protected default-media flag with per-type flags. Public fluent methods are unchanged. An SSR gateway without health-check support now reports that unsupported capability instead of incorrectly reporting that the server is down.

Shared framework support

The exception handler gains beforeResponding(), exposed through exception configuration and the facade. It observes the prepared response before the application's final response customization without replacing that callback. Head uses it to capture the actual status before an Inertia error page renders, including exceptions with embedded responses or custom renderers. The early status callback remains for ordinary Blade error views. Head registers these hooks only when the bound handler supports them, preserving custom exception handlers.

TestResponse also correctly reports invalid JSON from a plain Symfony response when no stored exception exists. Stored exceptions are still rethrown unchanged.

Performance and tooling

Route-attribute lookup and inferred schema types are cached by registered builder or loaded class, keeping worker memory bounded. Request values and rendered output are not cached across requests. JSON-LD is encoded once per render, and non-cryptographic content identifiers use deterministic xxh128 hashes.

Editor declarations and a package PHPStan extension cover every withHead routing surface without loading runtime macros or replacing existing routing types. The extension follows the framework's existing layout and uses public PHPStan reflection APIs. Generated facade metadata retains rich callback and generic types through the existing mixin support. Optional static cleanup integrates with the framework test lifecycle.

Validation

Applicable upstream tests are ported to PHPUnit, with additional coverage for coroutine isolation, context copying, runtime configuration, rendering fixes, exception responses, and SSR integration. Formatting, source and type analysis, Head tests, facade checks, package-mode tests, and dogfood checks pass.

The full framework suite was also exercised, with affected checks repeated after corrections. External-service tests without configured services were skipped.

The upstream Head guide is included and adapted for Hypervel, with package navigation, an Inertia link, and guidance for porting worker-shared configuration.

[!NOTE]

Add Head package for Blade and Inertia head metadata management

  • Ports the Laravel Head package to Hypervel as a new hypervel/head package with a Head facade, service provider, and Blade @head directive
  • Covers title, description, meta tags, canonical URLs, robots, Open Graph, Twitter cards, resource hints, pagination and alternate links, and JSON-LD schemas with a SchemaFactory and built-in schema types like Article, Event, Product, and FAQ
  • Adds route-level metadata via the withHead macro on Router, Route, RouteRegistrar, and resource registrations, layered as defaults, route, page, and error definitions
  • Integrates with Inertia: shares head elements as a page prop, deduplicates them with stable data-inertia keys, and removes page-managed elements from SSR responses through UniqueHeadGateway
  • Adds error-page head metadata driven by the exception status, using new beforeResponding hooks in the exception handler; includes PHPStan extension support and full feature test coverage
  • Behavioral Change: exception handling now runs registered beforeResponding callbacks before final response customization (see Handler::finalizeRenderedResponse), and Inertia SSR responses no longer include page-managed head elements

Macroscope summarized 54414c4.