Skip to content

Add Head for Blade and Inertia metadata management - #68

Merged
binaryfire merged 15 commits into
0.4from
feature/head
Oct 21, 2026
Merged

binaryfire merged 15 commits into
0.4from
feature/head

Conversation

@binaryfire

@binaryfire binaryfire commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

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.

View guided diff Turn on auto-fix

Note

Add Head package for Blade and Inertia head metadata management

  • Ports a full Head package to Hypervel: fluent head tag builders (title, description, canonical, robots, Open Graph, Twitter, links, feeds, resource hints, pagination), a JSON-LD schema factory with Schema.org types, layered head data resolution with defaults/route/page/error precedence, and a withHead route macro across all routing surfaces.
  • Integrates with Blade via a @head directive and with Inertia via the ShareHead middleware and UniqueHeadGateway SSR decorator, so PHP-managed metadata renders into the initial document and is shared with Inertia navigation.
  • Adds supporting framework hooks: Exceptions::beforeResponding callbacks on the exception handler so Head can capture the HTTP status for error-page metadata, plus PHPStan extensions and IDE helpers for the route macro.
  • Includes full docs in head.md and the port plan in 2026-10-21-head-port.md.
  • Behavioral Change: exceptions and responses now report their status to HeadManager via the new beforeResponding hook in Handler.php; route head metadata rejects unknown names or invalid values with InvalidArgumentException instead of ignoring them.

Macroscope summarized 54414c4.

Allow packages to inspect the prepared exception response without replacing the application response callback. Invoke boot-registered observers after request-local response callbacks and before respondUsing, ignoring observer return values.

Expose beforeResponding through exception configuration and the facade, document its worker lifetime, and cover ordering, response identity, arguments, and ignored return values in the handler tests. This lets metadata integrations observe the actual error status before an Inertia error page renders.

Validation: formatting and source/types analysis passed; FoundationExceptionsHandlerTest passed with 78 tests and 230 assertions. The complete framework run had no behavioral failures; its sole facade metadata failure was corrected and verified separately.
Read the optional response exception through null coalescing before rethrowing it. Plain Symfony responses have no exception property, so invalid JSON previously raised an undefined-property error instead of the intended assertion failure.

Keep stored exception identity and the existing response proxy behavior unchanged. Add an exact-message regression alongside the stored-exception coverage.

Validation: TestResponseTest passed with 259 tests and 1,030 assertions; formatting and source/types analysis passed.
Port Laravel Head metadata builders, routing macros, structured data, facades, and rendering into the optional hypervel/head split package. Keep request data scoped and replicable while reusing worker-safe services and bounded metadata caches.

Add boot-registered runtime defaults, error, and canonical-origin resolvers for request-specific metadata. Preserve eager upstream configuration, isolate copied schemas, and retain lazy Inertia sharing and partial reload behavior. Decorate SSR responses without mutating their readonly contents or changing the JavaScript protocol, and forward gateway capabilities.

Correct canonical query and fragment handling, independent Open Graph fallback media, literal zero metadata, manifest replacement, title ownership, and shared schema-factory lookup. Capture actual exception response status before final customization. Wire split and development metadata, optional test cleanup, editor signatures, generated facade metadata, and PHPStan macro support using public reflection APIs.

Validation: formatting and complete source/types analysis passed, including routing and facade fixtures. Head passed 180 tests and 640 assertions; facade metadata checks passed 2 tests and 16,228 assertions. Package-mode and dogfood checks passed. The initial full framework run completed with one facade metadata failure, subsequently fixed and verified by focused checks.
Convert every applicable upstream Pest case to PHPUnit while preserving the upstream layout and fixture coverage. Record the intentional Livewire exclusion and use the shared Hypervel test lifecycle for static cleanup.

Add focused coverage for concurrent requests, copied contexts, mutable builder lifetimes, lazy runtime resolvers, canonical origins, independent media fallback, manifest ownership, schema factories, and literal zero values. Exercise optional provider discovery, early handler and kernel resolution, actual exception status, SSR capability forwarding, custom props, partial reloads, and both Blade SSR ordering paths.

Validation: all 180 Head cases passed with 640 assertions. Test files were checked during porting, and the complete framework run exposed no Head failures. No external services or new test framework are required for these tests.
Port the upstream Head guide with Hypervel installation, routing, Blade, structured data, and Inertia setup. Add request-specific defaults, error metadata, canonical origins, worker-lifetime guidance, context copying, and PHPStan integration using generic team examples.

Explain serverHead setup for client and SSR entry points, static globals, partial reload behavior, and application-owned settings retrieval. Add Head in the package navigation, link from Inertia, and describe the worker configuration adaptation in the Laravel porting guide.

Retain the completed implementation plan as the design reference. Documentation and comments were reviewed against the source, upstream coverage, and supported integration paths; diff whitespace checks passed.
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: hypervel/components-backup/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 838a18db-d546-4cb7-b29a-bb4babde0148

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add worker-safe Head metadata management for Blade and Inertia

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add fluent page metadata, route attributes, and JSON-LD schemas with coroutine-isolated state.
• Render metadata through Blade and optional Inertia integration, including SSR deduplication.
• Add exception-status observation, rendering fixes, documentation, static-analysis support, and
 regression tests.
Diagram

graph TD
  Routes["Route metadata"] --> Manager["Head manager"] --> Renderer["Head renderer"] --> Share["Inertia sharing"] --> Gateway["SSR gateway"]
  State["Coroutine state"] --> Manager
  Manager --> Builders["Tag builders"] --> Renderer
  Renderer --> Blade["Blade directive"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Deduplicate only in the Inertia client
  • ➕ Avoids decorating the SSR gateway.
  • ➖ Cannot reliably prevent duplicate tags in initial server-rendered HTML.
  • ➖ Would require a JavaScript integration or protocol change.
2. Store all metadata on a request-scoped manager
  • ➕ Makes mutable state ownership more obvious.
  • ➖ Complicates boot-time configuration and shared service registration.
  • ➖ Requires re-creating or transferring configuration for each request.

Recommendation: Keep the shared manager with separate coroutine-scoped page state, and retain gateway-level SSR deduplication. This preserves boot configuration and Inertia's existing client behavior while addressing concurrent requests and initial-document duplicates.

Files changed (119) +12498 / -2

Enhancement (65) +7763 / -0
Exceptions.phpExpose exception response observers in configuration +17/-0

Expose exception response observers in configuration

• Forwards beforeResponding registrations to the exception handler.

src/foundation/src/Configuration/Exceptions.php

Handler.phpObserve prepared exception responses +26/-0

Observe prepared exception responses

• Runs registered observers before final response customization without using their return values.

src/foundation/src/Exceptions/Handler.php

_ide_helper.phpDeclare Head editor methods +464/-0

Declare Head editor methods

• Supplies rich IDE signatures for Head APIs and routing macros.

src/head/_ide_helper.php

BuildsHead.phpProvide the fluent metadata API +504/-0

Provide the fluent metadata API

• Shares fluent methods for titles, social metadata, links, schemas, and other tags between manager and builder.

src/head/src/Concerns/BuildsHead.php

CurrentHead.phpIsolate active page metadata +69/-0

Isolate active page metadata

• Stores page data and status in replicable request scope, with a flush operation.

src/head/src/CurrentHead.php

ImageType.phpDefine image type values +16/-0

Define image type values

• Adds typed image formats for metadata builders.

src/head/src/Enums/ImageType.php

Media.phpDefine media-query values +13/-0

Define media-query values

• Adds typed media values for metadata APIs.

src/head/src/Enums/Media.php

OfferAvailability.phpDefine offer availability values +29/-0

Define offer availability values

• Adds typed availability choices for product offers.

src/head/src/Enums/OfferAvailability.php

OgType.phpDefine Open Graph types +21/-0

Define Open Graph types

• Adds typed Open Graph content classifications.

src/head/src/Enums/OgType.php

RobotsRule.phpDefine robots directives +63/-0

Define robots directives

• Provides typed search-engine indexing directives.

src/head/src/Enums/RobotsRule.php

TwitterCard.phpDefine Twitter card types +13/-0

Define Twitter card types

• Adds typed Twitter card choices.

src/head/src/Enums/TwitterCard.php

ErrorPages.phpConfigure error-page metadata +80/-0

Configure error-page metadata

• Stores status-specific Head definitions for later resolution.

src/head/src/ErrorPages.php

InvalidSchema.phpIdentify invalid schemas +11/-0

Identify invalid schemas

• Adds a dedicated exception for malformed JSON-LD definitions.

src/head/src/Exceptions/InvalidSchema.php

Head.phpExpose the Head facade +102/-0

Expose the Head facade

• Provides the public entry point and documented manager methods.

src/head/src/Facades/Head.php

Schema.phpExpose the Schema facade +62/-0

Expose the Schema facade

• Provides facade access to registered structured-data types.

src/head/src/Facades/Schema.php

HeadBuilder.phpAdd standalone Head definitions +43/-0

Add standalone Head definitions

• Lets configuration callbacks compose metadata against fresh data and the registered schema factory.

src/head/src/HeadBuilder.php

HeadData.phpImplement layered builder data +118/-0

Implement layered builder data

• Stores builders and merges or replicates them for precedence and coroutine isolation.

src/head/src/HeadData.php

HeadManager.phpResolve and render request metadata +375/-0

Resolve and render request metadata

• Combines boot defaults, runtime resolvers, route layers, page data, and error metadata; exposes Blade and Inertia rendering.

src/head/src/HeadManager.php

HeadServiceProvider.phpBoot Head integrations +81/-0

Boot Head integrations

• Registers shared services and scoped state, the Blade directive, route macros, exception-status capture, and optional Inertia support.

src/head/src/HeadServiceProvider.php

Integration.phpConditionally enable Inertia support +67/-0

Conditionally enable Inertia support

• Registers lazy sharing middleware and decorates SSR only when the Inertia provider is loaded.

src/head/src/Inertia/Integration.php

ShareHead.phpShare lazy Inertia head props +39/-0

Share lazy Inertia head props

• Supplies page-owned elements without forcing them into partial reloads.

src/head/src/Inertia/ShareHead.php

UniqueHeadGateway.phpRemove duplicate SSR-owned tags +104/-0

Remove duplicate SSR-owned tags

• Returns an immutable SSR response without duplicated page elements while forwarding supported gateway controls.

src/head/src/Inertia/UniqueHeadGateway.php

RoutingMacros.phpDescribe typed routing macro arguments +80/-0

Describe typed routing macro arguments

• Defines a static-analysis signature for withHead attributes and named arguments.

src/head/src/PHPStan/RoutingMacros.php

WithHeadExtension.phpRecognize withHead across routing APIs +65/-0

Recognize withHead across routing APIs

• Exposes the macro through PHPStan reflection with appropriate receiving-object return types.

src/head/src/PHPStan/WithHeadExtension.php

WithHeadMethodReflection.phpPreserve routing method reflection +155/-0

Preserve routing method reflection

• Adapts the reflected macro signature and return type for each routing surface.

src/head/src/PHPStan/WithHeadMethodReflection.php

HeadRenderer.phpRender resolved heads for Blade and Inertia +164/-0

Render resolved heads for Blade and Inertia

• Produces document HTML and keyed Inertia elements, and removes matching page elements from SSR head output.

src/head/src/Rendering/HeadRenderer.php

ResolvedHead.phpProvide rendering-time metadata lookups +169/-0

Provide rendering-time metadata lookups

• Exposes resolved title, description, canonical URL, fallbacks, and ordered tag builders.

src/head/src/Rendering/ResolvedHead.php

TagRenderer.phpRender escaped tags with ownership keys +221/-0

Render escaped tags with ownership keys

• Generates HTML elements and JSON-LD with stable, distinct Inertia identities.

src/head/src/Rendering/TagRenderer.php

HeadRouteMacros.phpAdd withHead to routing surfaces +462/-0

Add withHead to routing surfaces

• Registers typed metadata macros for routers, registrars, routes, and resource registrations.

src/head/src/Routing/HeadRouteMacros.php

RouteAttributeParser.phpApply layered route metadata +192/-0

Apply layered route metadata

• Normalizes named attributes and applies cache-friendly route layers through registered builders.

src/head/src/Routing/RouteAttributeParser.php

Article.phpAdd Article schema builder +62/-0

Add Article schema builder

• Provides fluent article structured-data properties.

src/head/src/Schema/Article.php

BlogPosting.phpAdd BlogPosting schema builder +12/-0

Add BlogPosting schema builder

• Specializes article structured data for blog posts.

src/head/src/Schema/BlogPosting.php

Brand.phpAdd Brand schema builder +35/-0

Add Brand schema builder

• Provides typed brand structured-data properties.

src/head/src/Schema/Brand.php

Breadcrumbs.phpAdd breadcrumb schema builder +43/-0

Add breadcrumb schema builder

• Builds structured breadcrumb navigation data.

src/head/src/Schema/Breadcrumbs.php

Event.phpAdd Event schema builder +70/-0

Add Event schema builder

• Provides fluent event structured-data properties.

src/head/src/Schema/Event.php

Faq.phpAdd FAQ schema builder +45/-0

Add FAQ schema builder

• Builds FAQ structured-data entries.

src/head/src/Schema/Faq.php

GenericSchemaObject.phpSupport arbitrary schema types +9/-0

Support arbitrary schema types

• Represents schema.org types without dedicated classes.

src/head/src/Schema/GenericSchemaObject.php

Offer.phpAdd Offer schema builder +36/-0

Add Offer schema builder

• Provides structured price and availability data.

src/head/src/Schema/Offer.php

Organization.phpAdd Organization schema builder +35/-0

Add Organization schema builder

• Provides fluent organization structured-data properties.

src/head/src/Schema/Organization.php

Person.phpAdd Person schema builder +27/-0

Add Person schema builder

• Provides fluent person structured-data properties.

src/head/src/Schema/Person.php

Product.phpAdd Product schema builder +63/-0

Add Product schema builder

• Provides product structured-data properties and related objects.

src/head/src/Schema/Product.php

SchemaFactory.phpCreate registered schema types +90/-0

Create registered schema types

• Registers built-in and custom types, returning fresh typed or generic schema objects.

src/head/src/Schema/SchemaFactory.php

SchemaObject.phpImplement fluent schema objects +147/-0

Implement fluent schema objects

• Coerces nested values and dates, serializes JSON-LD, and caches inferred types by class.

src/head/src/Schema/SchemaObject.php

SchemaValidator.phpValidate JSON-LD definitions +68/-0

Validate JSON-LD definitions

• Rejects invalid schemas outside production and logs warnings in production.

src/head/src/Schema/SchemaValidator.php

WebPage.phpAdd WebPage schema builder +35/-0

Add WebPage schema builder

• Provides fluent webpage structured-data properties.

src/head/src/Schema/WebPage.php

WebSite.phpAdd WebSite schema builder +27/-0

Add WebSite schema builder

• Provides fluent website structured-data properties.

src/head/src/Schema/WebSite.php

SchemaType.phpDeclare explicit schema types +18/-0

Declare explicit schema types

• Adds an attribute for mapping schema classes to schema.org type names.

src/head/src/SchemaType.php

TagRegistry.phpRegister and validate tag builders +120/-0

Register and validate tag builders

• Orders built-ins and extensions, caches route keys, and rejects invalid or conflicting registrations.

src/head/src/TagRegistry.php

AlternateLinks.phpBuild alternate-language links +113/-0

Build alternate-language links

• Collects and renders alternate URL declarations.

src/head/src/Tags/AlternateLinks.php

Canonical.phpResolve canonical URLs +216/-0

Resolve canonical URLs

• Supports explicit and automatic URLs, configurable origins, and path normalization without changing queries or fragments.

src/head/src/Tags/Canonical.php

Description.phpBuild page descriptions +33/-0

Build page descriptions

• Stores and renders description metadata, including the literal value "0".

src/head/src/Tags/Description.php

FeedLinks.phpBuild syndication feed links +193/-0

Build syndication feed links

• Collects and renders feed link metadata.

src/head/src/Tags/FeedLinks.php

GenericLinks.phpBuild general and app links +263/-0

Build general and app links

• Handles icons, manifests, and other links; later manifests replace earlier ones.

src/head/src/Tags/GenericLinks.php

GroupedTagBuilder.phpBase grouped tag builders +17/-0

Base grouped tag builders

• Provides common behavior for builders containing multiple tags.

src/head/src/Tags/GroupedTagBuilder.php

MetaTags.phpBuild named and property metadata +228/-0

Build named and property metadata

• Supports generic meta entries and aliases such as theme color and viewport.

src/head/src/Tags/MetaTags.php

OpenGraph.phpBuild Open Graph cards +488/-0

Build Open Graph cards

• Resolves social properties and independently layered image, video, and audio defaults.

src/head/src/Tags/OpenGraph.php

PaginationLinks.phpBuild pagination links +114/-0

Build pagination links

• Renders previous and next page relationships.

src/head/src/Tags/PaginationLinks.php

PerformanceLinks.phpBuild resource-hint links +438/-0

Build resource-hint links

• Supports preload, prefetch, preconnect, and DNS-prefetch metadata.

src/head/src/Tags/PerformanceLinks.php

Robots.phpBuild robots metadata +87/-0

Build robots metadata

• Combines typed or textual indexing directives into rendered metadata.

src/head/src/Tags/Robots.php

Schemas.phpCollect JSON-LD schemas +159/-0

Collect JSON-LD schemas

• Merges and deduplicates schema definitions before validation and rendering.

src/head/src/Tags/Schemas.php

StringTagBuilder.phpBase scalar tag builders +76/-0

Base scalar tag builders

• Provides shared behavior for single-string metadata values.

src/head/src/Tags/StringTagBuilder.php

TagBuilder.phpDefine the tag extension contract +165/-0

Define the tag extension contract

• Establishes merge, route-attribute, array-conversion, and rendering hooks for metadata builders.

src/head/src/Tags/TagBuilder.php

Title.phpBuild document titles +142/-0

Build document titles

• Supports inherited prefixes and suffixes, exact titles, and literal "0" values.

src/head/src/Tags/Title.php

Twitter.phpBuild Twitter card metadata +263/-0

Build Twitter card metadata

• Renders card properties and image fallbacks from resolved page metadata.

src/head/src/Tags/Twitter.php

Exceptions.phpExpose beforeResponding on the facade +1/-0

Expose beforeResponding on the facade

• Adds the exception response observer to facade method declarations.

src/support/src/Facades/Exceptions.php

Bug fix (1) +2 / -2
TestResponse.phpHandle invalid JSON without a stored exception +2/-2

Handle invalid JSON without a stored exception

• Uses null-safe exception lookup so plain Symfony responses produce the intended assertion failure.

src/testing/src/TestResponse.php

Documentation (8) +1235 / -0
2026-10-21-head-port.mdRecord the Head port plan +380/-0

Record the Head port plan

• Documents the port's design, integration boundaries, and validation plan.

docs/plans/2026-10-21-head-port.md

documentation.mdLink the Head guide +1/-0

Link the Head guide

• Adds Head to the package documentation navigation.

src/docs/documentation.md

errors.mdDocument exception response observers +18/-0

Document exception response observers

• Explains beforeResponding and its ordering and worker lifetime.

src/docs/errors.md

head.mdAdd the Head usage guide +793/-0

Add the Head usage guide

• Covers installation, fluent metadata, schemas, Blade, Inertia, SSR, and safe worker configuration.

src/docs/head.md

inertia.mdLink Inertia's Head integration +2/-0

Link Inertia's Head integration

• Points Inertia users to Head metadata guidance.

src/docs/inertia.md

porting-from-laravel.mdExplain Head porting considerations +6/-0

Explain Head porting considerations

• Highlights differences relevant to long-lived workers and rendering integrations.

src/docs/porting-from-laravel.md

LICENSE.mdAdd package license +23/-0

Add package license

• Includes the Head package's license terms.

src/head/LICENSE.md

README.mdIntroduce the standalone Head package +12/-0

Introduce the standalone Head package

• Provides a short package overview and documentation entry point.

src/head/README.md

Other (45) +3498 / -0
composer.jsonRegister Head in the monorepo +4/-0

Register Head in the monorepo

• Adds Head package autoloading and development wiring.

composer.json

phpstan.neon.distLoad Head PHPStan support +2/-0

Load Head PHPStan support

• Includes the Head extension in source analysis.

phpstan.neon.dist

phpstan.types.neon.distEnable Head type-analysis support +2/-0

Enable Head type-analysis support

• Includes Head's static-analysis configuration for type checks.

phpstan.types.neon.dist

composer.jsonDefine the distributable Head package +73/-0

Define the distributable Head package

• Declares dependencies, optional Inertia support, autoloading, provider discovery, and PHPStan integration.

src/head/composer.json

extension.neonRegister Head's PHPStan extension +5/-0

Register Head's PHPStan extension

• Wires method reflection for withHead routing calls.

src/head/extension.neon

AfterEachTestSubscriber.phpClear Head static state after tests +11/-0

Clear Head static state after tests

• Flushes Head macros, canonical origin configuration, and inferred schema types during framework test cleanup.

src/testing/src/PHPUnit/AfterEachTestSubscriber.php

FoundationExceptionsHandlerTest.phpTest exception response observer ordering +39/-0

Test exception response observer ordering

• Covers callback arguments, response identity, ordering, and ignored return values.

tests/Foundation/FoundationExceptionsHandlerTest.php

ArchTest.phpEnforce Head source conventions +82/-0

Enforce Head source conventions

• Checks strict typing and prohibits debugging and direct environment calls.

tests/Head/ArchTest.php

CoroutineIsolationTest.phpTest concurrent Head isolation +156/-0

Test concurrent Head isolation

• Verifies independent coroutine state and copied child-context metadata.

tests/Head/CoroutineIsolationTest.php

ErrorPagesTest.phpTest error metadata resolution +166/-0

Test error metadata resolution

• Covers status-specific definitions, runtime resolvers, and exception-response status capture.

tests/Head/Feature/ErrorPagesTest.php

HeadBuilderTest.phpTest standalone metadata builders +100/-0

Test standalone metadata builders

• Exercises fluent definitions and registered schema-factory behavior.

tests/Head/Feature/HeadBuilderTest.php

HeadDataTest.phpTest metadata layer merging +72/-0

Test metadata layer merging

• Checks builder overlays, defaults, and independent state copies.

tests/Head/Feature/HeadDataTest.php

HeadManagerTest.phpTest manager configuration and resolution +161/-0

Test manager configuration and resolution

• Covers layer precedence, runtime defaults, rendering, and flush behavior.

tests/Head/Feature/HeadManagerTest.php

HeadServiceProviderTest.phpTest Head provider registration +87/-0

Test Head provider registration

• Checks service bindings, Blade registration, and optional integration setup.

tests/Head/Feature/HeadServiceProviderTest.php

InertiaCustomPropTest.phpTest custom Inertia prop names +39/-0

Test custom Inertia prop names

• Verifies sharing and SSR behavior when Head uses a non-default prop.

tests/Head/Feature/InertiaCustomPropTest.php

InertiaTest.phpTest Inertia and SSR metadata flow +444/-0

Test Inertia and SSR metadata flow

• Covers initial documents, visits, partial reloads, tag ownership, deduplication, and gateway forwarding.

tests/Head/Feature/InertiaTest.php

HeadRendererTest.phpTest document and Inertia rendering +148/-0

Test document and Inertia rendering

• Checks rendered elements, stable ownership, and SSR duplicate removal.

tests/Head/Feature/Rendering/HeadRendererTest.php

TagRendererTest.phpTest individual HTML tag rendering +95/-0

Test individual HTML tag rendering

• Covers escaping, attributes, JSON-LD, and Inertia identities.

tests/Head/Feature/Rendering/TagRendererTest.php

RouteHeadTest.phpTest route metadata application +221/-0

Test route metadata application

• Covers route layers, attribute parsing, extension validation, and precedence.

tests/Head/Feature/Routing/RouteHeadTest.php

WithHeadTest.phpTest withHead routing macros +113/-0

Test withHead routing macros

• Exercises group, route, and resource registration calls.

tests/Head/Feature/Routing/WithHeadTest.php

SchemaTest.phpTest structured-data APIs +215/-0

Test structured-data APIs

• Covers schema creation, nesting, validation, registration, and JSON-LD rendering.

tests/Head/Feature/Schema/SchemaTest.php

CanonicalTest.phpTest canonical URL normalization +129/-0

Test canonical URL normalization

• Covers origins, base paths, console rendering, and preservation of query strings and fragments.

tests/Head/Feature/Tags/CanonicalTest.php

GenericLinksTest.phpTest generic link behavior +96/-0

Test generic link behavior

• Covers link aliases, merging, and manifest replacement.

tests/Head/Feature/Tags/GenericLinksTest.php

MetaTagsTest.phpTest generic metadata tags +131/-0

Test generic metadata tags

• Checks aliases, attribute combinations, and metadata merging.

tests/Head/Feature/Tags/MetaTagsTest.php

OpenGraphTest.phpTest Open Graph metadata +131/-0

Test Open Graph metadata

• Covers properties, media rendering, fallbacks, and per-media default precedence.

tests/Head/Feature/Tags/OpenGraphTest.php

PerformanceLinksTest.phpTest resource-hint links +69/-0

Test resource-hint links

• Checks performance link variants and rendered attributes.

tests/Head/Feature/Tags/PerformanceLinksTest.php

PwaTest.phpTest PWA link helpers +31/-0

Test PWA link helpers

• Checks app-oriented icons and manifest output.

tests/Head/Feature/Tags/PwaTest.php

RobotsTest.phpTest robots directives +70/-0

Test robots directives

• Verifies directive composition and output.

tests/Head/Feature/Tags/RobotsTest.php

ThemeColorTest.phpTest theme-color metadata +64/-0

Test theme-color metadata

• Checks media-specific theme colors and overrides.

tests/Head/Feature/Tags/ThemeColorTest.php

TitleTest.phpTest title formatting +41/-0

Test title formatting

• Covers title inheritance, exact rendering, and edge-case values.

tests/Head/Feature/Tags/TitleTest.php

TwitterTest.phpTest Twitter cards +91/-0

Test Twitter cards

• Checks card fields, images, and title or description fallbacks.

tests/Head/Feature/Tags/TwitterTest.php

ConflictingRouteAttribute.phpFixture for route-key conflicts +47/-0

Fixture for route-key conflicts

• Defines an extension with a conflicting route attribute for registry validation tests.

tests/Head/Fixtures/ConflictingRouteAttribute.php

JobPosting.phpFixture for custom schema types +29/-0

Fixture for custom schema types

• Defines a structured-data object used to test schema registration.

tests/Head/Fixtures/JobPosting.php

ReadingTime.phpFixture for custom tags +86/-0

Fixture for custom tags

• Defines an extension builder used in registration and rendering tests.

tests/Head/Fixtures/ReadingTime.php

ResolvesHttpKernelEarly.phpFixture for early kernel resolution +25/-0

Fixture for early kernel resolution

• Exercises middleware registration when the HTTP kernel is already resolved.

tests/Head/Fixtures/ResolvesHttpKernelEarly.php

app-directives.blade.phpFixture for Blade directive placement +10/-0

Fixture for Blade directive placement

• Provides an application layout for Head directive integration tests.

tests/Head/Fixtures/views/app-directives.blade.php

app-inertia-head-first.blade.phpFixture for Head-first Inertia layout +10/-0

Fixture for Head-first Inertia layout

• Exercises ownership and SSR deduplication when Head renders before Inertia tags.

tests/Head/Fixtures/views/app-inertia-head-first.blade.php

app-nested.blade.phpFixture for nested layout rendering +9/-0

Fixture for nested layout rendering

• Tests Head output within nested Blade views.

tests/Head/Fixtures/views/app-nested.blade.php

app.blade.phpFixture for application head output +10/-0

Fixture for application head output

• Provides a representative Blade and Inertia root layout.

tests/Head/Fixtures/views/app.blade.php

404.blade.phpFixture for error-page rendering +9/-0

Fixture for error-page rendering

• Provides a Blade 404 view for status-based metadata tests.

tests/Head/Fixtures/views/errors/404.blade.php

nested-head.blade.phpFixture for nested Head directives +1/-0

Fixture for nested Head directives

• Renders Head from a nested Blade view.

tests/Head/Fixtures/views/nested-head.blade.php

TestCase.phpSet up Head integration tests +36/-0

Set up Head integration tests

• Provides package providers and shared test configuration.

tests/Head/TestCase.php

TestResponseTest.phpRegress plain-response invalid JSON +11/-0

Regress plain-response invalid JSON

• Verifies the assertion message and preserves stored-exception rethrow behavior.

tests/Testing/TestResponseTest.php

Facades.phpCheck Head facade types +38/-0

Check Head facade types

• Exercises facade signatures and callback or generic inference.

types/Head/Facades.php

Route.phpCheck withHead routing types +89/-0

Check withHead routing types

• Tests named attributes and return types across routing surfaces.

types/Head/Route.php

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Custom error views miss error metadata 🐞 Bug ≡ Correctness
Description
registerExceptionStatusResolver() sets the status in a renderable callback or after the exception
response has been prepared, but exceptions with their own render() method or Responsable
response bypass that callback. When either path renders a Blade view containing @head, the view
resolves its metadata before beforeResponding records the response status, so configured error
metadata is absent.
Code

src/head/src/HeadServiceProvider.php[R70-76]

+            $handler->renderable(function (Throwable $exception, Request $request) use ($head): void {
+                $head->status(
+                    $exception instanceof HttpExceptionInterface ? $exception->getStatusCode() : 500
+                );
+            });
+
+            $handler->beforeResponding(function (Response $response) use ($head): void {
Evidence
The handler returns from the custom-render and Responsable branches before calling renderable
callbacks. It converts a returned view to a response before finalization; Head reads the recorded
status while rendering, whereas the new observer runs only during finalization.

src/foundation/src/Exceptions/Handler.php[815-859]
src/routing/src/Router.php[885-914]
src/head/src/HeadManager.php[298-308]
src/head/src/HeadServiceProvider.php[66-78]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Custom exception renderers can render `@head` before Head learns the response status.
## Fix Focus Areas
- src/head/src/HeadServiceProvider.php[64-79]
- src/foundation/src/Exceptions/Handler.php[815-859]
## Recommended Fix
Provide an exception-status hook that runs before custom exception responses render, while retaining the prepared-response observer for statuses only known later. Cover a custom `render()` view and a `Responsable` view.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Inertia provider subclasses lose Head ✓ Resolved
Description
Integration::register() checks whether the exact InertiaServiceProvider class name is loaded.
When an application registers a subclass instead, the provider registry stores the subclass name, so
Head silently skips both its shared prop middleware and SSR decoration.
Code

src/head/src/Inertia/Integration.php[R30-32]

+        if (! $this->app->providerIsLoaded(InertiaServiceProvider::class)) {
+            return;
+        }
Evidence
Integration checks one exact provider key, while the application records providers under their
concrete class names and performs an exact-key lookup.

src/head/src/Inertia/Integration.php[28-35]
src/foundation/src/Application.php[1117-1122]
src/foundation/src/Application.php[1294-1296]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An exact provider-name check disables Head integration when Inertia is registered through a subclass.
## Fix Focus Areas
- src/head/src/Inertia/Integration.php[28-35]
## Recommended Fix
Check loaded provider instances for `instanceof InertiaServiceProvider` rather than testing only the base class key. Cover registration through a subclass.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Nested invalid schemas pass validation 🐞 Bug ≡ Correctness
Description
Schemas::toHeadArray() validates raw array schemas without normalizing embedded SchemaObject
values, and SchemaValidator::containsEmptyValue() descends only into arrays. A raw schema array
containing a nested object with a null or empty property therefore passes validation and renders
that object through jsonSerialize(), which also adds an unwanted nested @context.
Code

src/head/src/Tags/Schemas.php[R108-111]

+        return array_map(function (SchemaObject|array $schema) use ($head): array {
+            $schemaData = $schema instanceof SchemaObject ? $schema->toJsonLd() : $schema;
+
+            $head->validateSchema($schemaData);
Evidence
Raw arrays reach validation unchanged; the validator does not inspect objects. The nested object's
JSON serialization adds its own context, unlike the context-free conversion used by SchemaObject for
nested assignments.

src/head/src/Tags/Schemas.php[106-126]
src/head/src/Schema/SchemaValidator.php[54-66]
src/head/src/Schema/SchemaObject.php[70-82]
src/head/src/Schema/SchemaObject.php[106-118]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Raw schema arrays can conceal invalid nested schema values and render nested contexts.
## Fix Focus Areas
- src/head/src/Tags/Schemas.php[106-114]
- src/head/src/Schema/SchemaValidator.php[54-64]
## Recommended Fix
Recursively normalize embedded `SchemaObject` values to their context-free arrays before validating and rendering raw schema arrays. Test an embedded object containing an empty property.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Numeric schema names crash page building ✓ Resolved
Description
Faq::questions() and Breadcrumbs::items() pass each array key directly to methods requiring a
string. PHP converts numeric-string keys to integers, so an otherwise valid named question or
breadcrumb such as '2024' raises a TypeError under these files' strict typing.
Code

src/head/src/Schema/Faq.php[R39-40]

+        foreach ($questions as $name => $answer) {
+            $this->question($name, $answer);
Evidence
Both bulk helpers forward array keys into strictly typed string parameters without conversion; both
files declare strict types.

src/head/src/Schema/Faq.php[3-3]
src/head/src/Schema/Faq.php[18-18]
src/head/src/Schema/Faq.php[35-41]
src/head/src/Schema/Breadcrumbs.php[18-18]
src/head/src/Schema/Breadcrumbs.php[33-39]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Numeric-string keys become integer keys and crash the bulk schema helpers.
## Fix Focus Areas
- src/head/src/Schema/Faq.php[37-41]
- src/head/src/Schema/Breadcrumbs.php[35-39]
## Recommended Fix
Cast each array key to a string before passing it to `question()` or `item()`. Test a numeric-string question and breadcrumb name.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (3)
5. Custom schema types gain spaces 🐞 Bug ≡ Correctness
Description
SchemaObject::typeFromAttribute() falls back to Str::headline() for a class without its own
SchemaType attribute. A custom subclass such as NewsArticle consequently emits `"@type":"News
Article"` rather than a schema type identifier, and a subclass of an attributed built-in schema also
loses its parent's type.
Code

src/head/src/Schema/SchemaObject.php[R135-137]

+        return static::$types[static::class] = $attributes === []
+            ? Str::headline($reflection->getShortName())
+            : $attributes[0]->newInstance()->name;
Evidence
The fallback explicitly calls Str::headline, whose implementation inserts spaces into camel-cased
names. Attributes are fetched from the reflected concrete class, while built-in schema classes
declare their type on those classes.

src/head/src/Schema/SchemaObject.php[126-137]
src/support/src/Str.php[1324-1334]
src/head/src/Schema/Article.php[8-12]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The fallback formats custom schema class names as readable headlines rather than schema identifiers.
## Fix Focus Areas
- src/head/src/Schema/SchemaObject.php[124-137]
## Recommended Fix
Use a type-compatible class-name fallback and preserve an attributed parent type for subclasses that do not override it. Test both an unattributed custom class and a subclass of an attributed built-in type.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Uppercase canonical URLs use the wrong host ✓ Resolved
Description
Canonical::normalizeUrl() recognizes only lowercase HTTP scheme prefixes when deciding whether a
URL is absolute. An explicit URL beginning with HTTPS:// is treated as relative and prefixed with
the request host; an uppercase HTTP:// prefix also bypasses the requested HTTPS upgrade.
Code

src/head/src/Tags/Canonical.php[180]

+        $relative = ! str_starts_with($url, 'http://') && ! str_starts_with($url, 'https://');
Evidence
The case-sensitive prefix checks classify an uppercase scheme as relative, triggering request-origin
prepending; the HTTPS replacement is case-sensitive as well.

src/head/src/Tags/Canonical.php[178-194]
src/head/src/Tags/Canonical.php[93-105]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Absolute canonical URLs with uppercase schemes are normalized as relative URLs.
## Fix Focus Areas
- src/head/src/Tags/Canonical.php[178-194]
## Recommended Fix
Detect HTTP and HTTPS schemes case-insensitively and apply the same handling to the HTTPS upgrade. Add uppercase-scheme absolute URL cases.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Custom SSR gateways crash Inertia requests 🐞 Bug ☼ Reliability
Description
UniqueHeadGateway always implements ExcludesSsrPaths, DisablesSsr, ConfiguresSsrRequests,
and HasHealthCheck, so instanceof checks pass even when its forwarding methods will throw
LogicException because the wrapped gateway lacks the capability. With a custom dispatch-only
gateway, configured withoutSsr paths make Inertia middleware call except() on every request,
while inertia:check-ssr calls isHealthy() instead of taking its unsupported-capability branch;
HeadServiceProvider is auto-registered, so this also affects apps that do not use Head.
Code

src/head/src/Inertia/UniqueHeadGateway.php[R72-78]

+    public function except(array|string $paths): void
+    {
+        if (! $this->gateway instanceof ExcludesSsrPaths) {
+            throw new LogicException('The configured SSR gateway does not support excluding paths from server-side rendering.');
+        }
+
+        $this->gateway->except($paths);
Evidence
Middleware.php:159-162 guards its except() call with instanceof ExcludesSsrPaths, but the
decorator's class declaration at UniqueHeadGateway.php:19 makes that check pass regardless of the
wrapped gateway, and its forwarding method throws at lines 74-76 when exclusion is unsupported. The
command likewise checks HasHealthCheck before calling isHealthy(), which throws for an
unsupported wrapped gateway at lines 96-102. Integration::decorateSsrGateway wraps whichever
Gateway is bound, and the root composer.json auto-registers HeadServiceProvider.

src/head/src/Inertia/UniqueHeadGateway.php[19-79]
src/inertia/src/Middleware.php[159-162]
src/head/src/Inertia/Integration.php[59-66]
src/inertia/src/Commands/CheckSsr.php[28-41]
src/inertia/src/Ssr/Gateway.php[7-15]
src/inertia/src/Middleware.php[157-163]
src/head/src/Inertia/UniqueHeadGateway.php[19-25]
src/head/src/Inertia/UniqueHeadGateway.php[72-78]
src/inertia/src/Commands/CheckSsr.php[28-40]
src/head/src/Inertia/UniqueHeadGateway.php[19-19]
src/head/src/Inertia/UniqueHeadGateway.php[93-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`UniqueHeadGateway` advertises optional SSR capabilities even when the wrapped gateway does not support them. This causes middleware configured with `withoutSsr` paths to throw instead of skipping exclusions, and causes `inertia:check-ssr` to throw instead of taking its unsupported-capability branch.
## Fix Focus Areas
- src/head/src/Inertia/UniqueHeadGateway.php[19-104]
- src/head/src/Inertia/Integration.php[59-66]
## Recommended Fix
Make the base decorator implement only `Gateway`, and use capability-specific subclasses or decorator variants that expose each optional interface only when the wrapped gateway implements it. Select the appropriate variant in `Integration::decorateSsrGateway` using `instanceof` checks, while retaining forwarding for supported capabilities. Verify that middleware skips exclusions for a dispatch-only gateway and that the SSR check command takes its unsupported-capability branch for that gateway.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

8. Name and property meta tags drop each other ✓ Resolved
Description
MetaTags::tag() stores name and property forms of the same meta key separately, but
MetaTags::inertiaKey() gives both the bare key when no media is set. If a page defines both forms,
Inertia’s last-write-wins deduplication removes one from the document and navigation updates, even
though ordinary Blade rendering retains both.
Code

src/head/src/Tags/MetaTags.php[R191-196]

+    protected function inertiaKey(array $meta): string
+    {
+        if (! isset($meta['media'])) {
+            return self::inertiaAliases()[$meta['key']] ?? $meta['key'];
+        }
+
Evidence
tagKey includes the resolved attribute, so storage distinguishes the name and property forms.
The media-less branch of inertiaKey omits that attribute, assigning both the same ownership key;
HeadRenderer replaces an earlier element when the key repeats.

src/head/src/Tags/MetaTags.php[86-97]
src/head/src/Tags/MetaTags.php[136-144]
src/head/src/Tags/MetaTags.php[175-197]
src/head/src/Tags/MetaTags.php[86-95]
src/head/src/Tags/MetaTags.php[135-143]
src/head/src/Tags/MetaTags.php[169-197]
src/head/src/Rendering/HeadRenderer.php[99-125]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MetaTags::inertiaKey()` assigns the same `data-inertia` key to distinct `name` and `property` tags without media, causing Inertia to deduplicate one away.
## Fix Focus Areas
- src/head/src/Tags/MetaTags.php[177-197]
## Recommended Fix
Keep the bare key or intentional alias for the default RDFa-detected attribute. When `property` is explicitly set against that detected attribute, give the tag a distinct Inertia identity, such as `meta:` plus a hash of `tagKey(...)`. Add an Inertia-page test confirming that both forms of the same key survive deduplication.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Route hint maps with string values break URLs ✓ Resolved
Description
PerformanceLinks::repeatableLinkAttributes() returns a URL-keyed map unchanged, and
addPreloadRouteAttribute / addPrefetchRouteAttribute / addPreconnectRouteAttribute treat a
string value as the href and discard the key. For example, `prefetch: ['/images/next.webp' =>
'image'] renders <link rel="prefetch" href="image">` without any error, while the equivalent
FeedLinks map form treats the key as the URL.
Code

src/head/src/Tags/PerformanceLinks.php[R365-370]

+    private function addPrefetchRouteAttribute(mixed $href, mixed $attributes): bool
+    {
+        if (is_string($attributes)) {
+            $this->prefetch($attributes);
+
+            return true;
Evidence
For string values, the map key $href is ignored and the value is passed as the URL.
routeAttributeHref, which falls back to the key, is used only for array values.

src/head/src/Tags/PerformanceLinks.php[111-122]
src/head/src/Tags/PerformanceLinks.php[337-395]
src/head/src/Tags/PerformanceLinks.php[406-436]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Route-attribute maps such as `['/x.webp' => 'image']` for preload/prefetch/preconnect use the string value as the href and drop the URL key.
## Fix Focus Areas
- src/head/src/Tags/PerformanceLinks.php[337-395]
## Recommended Fix
In each `add*RouteAttribute`, when `$attributes` is a string and `$href` is a string key, treat the key as the URL and the value as `as` (preload/prefetch) or `crossorigin` (preconnect). Alternatively, reject the input by returning false. Keep the current behaviour only for integer keys (list form).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/head/src/HeadServiceProvider.php Outdated
Comment thread src/head/src/Inertia/Integration.php Outdated
Comment on lines +108 to +111
return array_map(function (SchemaObject|array $schema) use ($head): array {
$schemaData = $schema instanceof SchemaObject ? $schema->toJsonLd() : $schema;

$head->validateSchema($schemaData);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Nested invalid schemas pass validation 🐞 Bug ≡ Correctness

Schemas::toHeadArray() validates raw array schemas without normalizing embedded SchemaObject
values, and SchemaValidator::containsEmptyValue() descends only into arrays. A raw schema array
containing a nested object with a null or empty property therefore passes validation and renders
that object through jsonSerialize(), which also adds an unwanted nested @context.
Agent Prompt
## Issue description
Raw schema arrays can conceal invalid nested schema values and render nested contexts.
## Fix Focus Areas
- src/head/src/Tags/Schemas.php[106-114]
- src/head/src/Schema/SchemaValidator.php[54-64]
## Recommended Fix
Recursively normalize embedded `SchemaObject` values to their context-free arrays before validating and rendering raw schema arrays. Test an embedded object containing an empty property.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the validation gap in 42ffae1. The validator now descends into nested SchemaObject values through toArray(), and a regression covers invalid nested properties. Raw schema serialization stays unchanged: nested JSON-LD contexts are valid, and validation does not need to rewrite application-owned schema data.

Comment thread src/head/src/Schema/Faq.php Outdated
Comment on lines +135 to +137
return static::$types[static::class] = $attributes === []
? Str::headline($reflection->getShortName())
: $attributes[0]->newInstance()->name;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Custom schema types gain spaces 🐞 Bug ≡ Correctness

SchemaObject::typeFromAttribute() falls back to Str::headline() for a class without its own
SchemaType attribute. A custom subclass such as NewsArticle consequently emits `"@type":"News
Article"` rather than a schema type identifier, and a subclass of an attributed built-in schema also
loses its parent's type.
Agent Prompt
## Issue description
The fallback formats custom schema class names as readable headlines rather than schema identifiers.
## Fix Focus Areas
- src/head/src/Schema/SchemaObject.php[124-137]
## Recommended Fix
Use a type-compatible class-name fallback and preserve an attributed parent type for subclasses that do not override it. Test both an unattributed custom class and a subclass of an attributed built-in type.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 42ffae1. Type inference now preserves the concrete class short name, so names such as JobPosting remain valid schema identifiers. Explicit types and SchemaType attributes still take precedence. Regression coverage includes a multiword class name. Parent attributes are intentionally not inherited: a NewsArticle subclass should infer NewsArticle, not silently become Article.

Comment thread src/head/src/Tags/Canonical.php Outdated
Comment on lines +72 to +78
public function except(array|string $paths): void
{
if (! $this->gateway instanceof ExcludesSsrPaths) {
throw new LogicException('The configured SSR gateway does not support excluding paths from server-side rendering.');
}

$this->gateway->except($paths);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

7. Custom ssr gateways crash inertia requests 🐞 Bug ☼ Reliability

UniqueHeadGateway always implements ExcludesSsrPaths, DisablesSsr, ConfiguresSsrRequests,
and HasHealthCheck, so instanceof checks pass even when its forwarding methods will throw
LogicException because the wrapped gateway lacks the capability. With a custom dispatch-only
gateway, configured withoutSsr paths make Inertia middleware call except() on every request,
while inertia:check-ssr calls isHealthy() instead of taking its unsupported-capability branch;
HeadServiceProvider is auto-registered, so this also affects apps that do not use Head.
Agent Prompt
## Issue description
`UniqueHeadGateway` advertises optional SSR capabilities even when the wrapped gateway does not support them. This causes middleware configured with `withoutSsr` paths to throw instead of skipping exclusions, and causes `inertia:check-ssr` to throw instead of taking its unsupported-capability branch.

## Fix Focus Areas
- src/head/src/Inertia/UniqueHeadGateway.php[19-104]
- src/head/src/Inertia/Integration.php[59-66]

## Recommended Fix
Make the base decorator implement only `Gateway`, and use capability-specific subclasses or decorator variants that expose each optional interface only when the wrapped gateway implements it. Select the appropriate variant in `Integration::decorateSsrGateway` using `instanceof` checks, while retaining forwarding for supported capabilities. Verify that middleware skips exclusions for a dispatch-only gateway and that the SSR check command takes its unsupported-capability branch for that gateway.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the existing capability forwarding. Supported operations are delegated; asking a custom gateway to exclude paths or configure requests when it does not support that operation raises a clear error instead of silently ignoring the setting. Unsupported health checks report that capability is unavailable rather than incorrectly saying the server is down. No changes to the SSR response protocol are needed. Head is an optional split package. The root Composer provider entries are for monorepo development and do not install or enable Head in every application.

Comment thread src/head/src/Tags/MetaTags.php
Comment thread src/head/src/Tags/PerformanceLinks.php
@greptile-apps

greptile-apps Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: plus

[Medium impact] The PR appears safe to merge; all seven previous findings are addressed and no new actionable issues were found.

Summary

Adds hypervel/head for Blade and optional Inertia metadata, with request-local page state and worker-shared configuration.

  • Pages can build their document head from layered PHP metadata.
  • Routes can add page metadata with withHead().
  • Pages can add built-in or custom schema.org data as JSON-LD.
  • Inertia pages can share Head metadata and avoid duplicate SSR tags.
  • Exception setup can observe the prepared response before final customization.
  • Package setup and guides make Head available as a standalone Hypervel component.

Reviews (2) · Last reviewed commit: "Document canonical fallbacks and event s..." · Reviewed by Greptile

Comment thread src/head/src/Inertia/Integration.php Outdated
Comment thread src/head/src/HeadServiceProvider.php Outdated
Comment thread src/head/src/Tags/Canonical.php Outdated
Comment thread src/head/src/Tags/MetaTags.php Outdated
Comment thread src/head/src/Schema/SchemaObject.php Outdated
Comment thread src/head/src/Schema/SchemaObject.php
Comment thread src/head/src/TagRegistry.php
Use the supplied magic setter value regardless of its argument name, preserving explicit null instead of replacing it with true. Infer schema types from the concrete class name without inserting spaces or inheriting a parent type.

Restore numeric array keys to string labels in bulk breadcrumb and FAQ helpers. Descend into nested SchemaObject values during validation while preserving raw schema serialization and reference behavior.

Extend schema regressions for named and null values, multiword class names, numeric labels, and invalid nested objects. Formatting, source and type analysis, and the Head suite pass.
Recognize HTTP and HTTPS schemes without regard to case and keep protocol-relative URLs on their explicit host. Resolve their scheme from forced HTTPS or the current request, retaining the protocol-relative form when neither applies.

Keep origin callbacks limited to automatic and relative page URLs. Existing path, query, fragment, and trailing-slash normalization remains intact.

Cover uppercase schemes and protocol-relative URLs with and without a request. Canonical tests, formatting, and static analysis pass.
Include the attribute in the ownership identity when a meta tag explicitly uses a different name or property attribute from the inferred default. This prevents Inertia deduplication from dropping distinct tags with the same key.

Retain semantic ownership keys and aliases for normal tags so generic metadata continues to override dedicated builders. Media-specific tags keep their full identity.

Add focused cases for description, Open Graph, and aliased theme metadata. Meta rendering tests, the Head suite, and static analysis pass.
Return an already registered custom builder before checking route-attribute conflicts. Repeated registration no longer conflicts with its own keys or invalidates the cached attribute map.

Continue rejecting built-in builders, invalid extension classes, and keys owned by another builder. Extend the existing route integration regression to register the same builder twice and render its metadata.

The focused extension tests, Head suite, formatting, and static analysis pass.
Replace pagination relations as one complete set so moving to a last or single page cannot retain stale links. Accept cursor paginator contracts alongside ordinary paginators, regenerate facade metadata, and stop advertising pagination as a route attribute.

Accept bare feed URLs and lists as untitled RSS feeds while retaining titled maps and structured definitions. Treat omitted or null structured titles as untitled, omit empty title attributes, and continue rejecting other non-string titles.

Reject ambiguous URL-to-string resource-hint maps through the existing route validation path instead of rendering the value as a URL. Preserve supported scalar and structured list inputs.

Add focused rendering and route regressions for replacement, cursors, feed forms, and invalid inputs. Affected tests, the Head suite, facade checks, formatting, and source and type analysis pass.
Register lazy Head sharing before global middleware so Inertia error responses include metadata when maintenance handling rejects the request. Recognize registered Inertia provider subclasses while keeping discovery-disabled applications free of the integration.

Register exception status callbacks only when the bound handler supports each hook. Use the exception handler contract and callable checks so contract-only handlers and forwarding fakes remain supported without adding runtime lookup machinery.

Cover maintenance errors, a custom Inertia provider with a custom prop, and a handler without rendering hooks. Existing tests retain early kernel and handler resolution, SSR, and partial-reload coverage. Focused tests, the Head suite, formatting, and static analysis pass.
Explain complete pagination replacement, ordinary and cursor paginator support, accepted feed route forms, and omission of an empty fluent feed title.

Clarify that automatic exception status capture uses the framework exception handler. Keep correctness fixes and additive support in the feature guide without adding compatibility warnings to the Laravel porting guide.

Update the active port plan to reflect the completed correction design and the remaining release checkpoint.
Resolve an omitted og:url from the existing canonical builder when Open Graph metadata is present. Preserve explicit Open Graph URLs and leave output unchanged when no canonical or Open Graph builder exists.

Port the implementation and regression cases from laravel/head#14 while retaining null and empty-string checks. Also port the split-defaults regression from laravel/head#16; the existing per-media provenance flags already provide the fix.

The Head suite, source and type analysis, facade checks, and formatting pass. The change adds no cross-request state or cache.
Port the Event location, image, performer, status, and attendance-mode helpers from laravel/head#15, together with Place and the two schema.org enums. Preserve the proposed signatures and string-location conversion to a named Place, following existing typed schema conventions.

Register Place in the schema factory and regenerate its facade metadata. All schema builders continue to support raw values through set(), and nested values use the existing coercion path.

Copy and convert every upstream Event schema test to PHPUnit, covering rendering, location conversion, enums, related schemas, and factory resolution. Formatting, source and type analysis, the Head suite, and facade checks pass.
Extend the Open Graph fallback guidance to include the canonical URL and record the public output difference in the package README.

Document Event and Place authoring with a structured address, status and attendance enums, string-location behavior, and raw schema values through set(). Add the Events navigation entry and Place factory to the feature guide.

Record the agreed upstream additions and remaining PR workflow in the active port plan. Keep upstream source references there rather than in the package README.
@binaryfire
binaryfire merged commit b35298b into 0.4 Oct 21, 2026
51 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant