Skip to content

Add Head for Blade and Inertia metadata management - #683

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 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.

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/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: edd56c1c-0973-4331-ac9b-85b00948254e

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.

@binaryfire

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review skipped: 119 files exceed the limit of 100.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@binaryfire

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 21, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@binaryfire cubic can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 123,074 of the 120,000 allowed lines of code this month. Reviews resume on 10 November 2026 (in 21 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

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

Copy link
Copy Markdown

PR Summary by Qodo

Add worker-safe Head metadata for Blade and Inertia

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

Grey Divider

AI Description

• Add fluent document metadata, route definitions, and structured data for Blade and optional
 Inertia rendering.
• Isolate page state per coroutine and resolve request-dependent defaults without leaking metadata
 between requests.
• Preserve Inertia head ownership and SSR output; expand exception hooks, tests, typing, and
 documentation.
Diagram

graph TD
  Provider["Head provider"] --> Manager["Head manager"] --> State["Coroutine state"]
  Routes["Route metadata"] --> Manager
  Manager --> Builders["Tag builders"] --> Renderer["Head renderer"] --> Blade["Blade views"]
  Renderer --> Inertia["Inertia and SSR"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Integrate directly into Inertia rendering
  • ➕ Could avoid the SSR gateway decorator and duplicate-removal step.
  • ➖ Couples an optional package to Inertia internals and requires modifying its rendering path.
2. Keep metadata on the shared manager
  • ➕ Simpler state model.
  • ➖ Mutable metadata could leak between concurrent requests in long-lived workers.

Recommendation: Retain coroutine-local state and optional gateway decoration: these preserve worker isolation and the existing Inertia protocol. Review SSR deduplication and exception-status ordering closely.

Files changed (119) +12498 / -2

Enhancement (65) +7763 / -0
Exceptions.phpConfigure exception response observers +17/-0

Configure exception response observers

• Exposes beforeResponding through exception configuration.

src/foundation/src/Configuration/Exceptions.php

Handler.phpObserve prepared exception responses +26/-0

Observe prepared exception responses

• Invokes observers before final response customization without adopting their return values.

src/foundation/src/Exceptions/Handler.php

_ide_helper.phpDeclare Head APIs for editors +464/-0

Declare Head APIs for editors

• Provides facade and routing-macro editor declarations.

src/head/_ide_helper.php

BuildsHead.phpProvide fluent metadata definitions +504/-0

Provide fluent metadata definitions

• Shares title, social-card, link, robots, and schema methods between manager and builder.

src/head/src/Concerns/BuildsHead.php

CurrentHead.phpIsolate current page metadata +69/-0

Isolate current page metadata

• Stores scoped data and status with flush and deep coroutine-context replication.

src/head/src/CurrentHead.php

ImageType.phpDefine image MIME types +16/-0

Define image MIME types

• Provides typed image formats for metadata and links.

src/head/src/Enums/ImageType.php

Media.phpDefine common media queries +13/-0

Define common media queries

• Provides light, dark, portrait, and landscape values.

src/head/src/Enums/Media.php

OfferAvailability.phpDefine offer availability +29/-0

Define offer availability

• Enumerates schema.org item availability values.

src/head/src/Enums/OfferAvailability.php

OgType.phpDefine Open Graph types +21/-0

Define Open Graph types

• Provides typed website, article, music, and video values.

src/head/src/Enums/OgType.php

RobotsRule.phpDefine robots directives +63/-0

Define robots directives

• Provides typed indexing and crawling rules.

src/head/src/Enums/RobotsRule.php

TwitterCard.phpDefine Twitter card types +13/-0

Define Twitter card types

• Provides summary, image, app, and player card values.

src/head/src/Enums/TwitterCard.php

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

Build error-page metadata

• Combines shared defaults with status-specific named attributes or callbacks.

src/head/src/ErrorPages.php

InvalidSchema.phpIdentify invalid schemas +11/-0

Identify invalid schemas

• Adds an exception for invalid structured data.

src/head/src/Exceptions/InvalidSchema.php

Head.phpExpose fluent Head facade +102/-0

Expose fluent Head facade

• Declares typed manager configuration, metadata, and rendering calls.

src/head/src/Facades/Head.php

Schema.phpExpose schema facade +62/-0

Expose schema facade

• Provides typed access to schema factory methods.

src/head/src/Facades/Schema.php

HeadBuilder.phpBuild independent definitions +43/-0

Build independent definitions

• Uses fluent metadata and resolves registered schema types when constructed directly.

src/head/src/HeadBuilder.php

HeadData.phpMerge layered tag data +118/-0

Merge layered tag data

• Stores and overlays builders while supporting independent mutable copies.

src/head/src/HeadData.php

HeadManager.phpResolve worker-safe metadata +375/-0

Resolve worker-safe metadata

• Combines defaults, routes, page, and errors; exposes Blade and Inertia rendering and scoped flush.

src/head/src/HeadManager.php

HeadServiceProvider.phpWire Head into Hypervel +81/-0

Wire Head into Hypervel

• Registers services, Blade directive, route macros, exception observers, and optional Inertia integration.

src/head/src/HeadServiceProvider.php

Integration.phpConditionally enable Inertia +67/-0

Conditionally enable Inertia

• Installs middleware and SSR decoration only when Inertia's provider is registered.

src/head/src/Inertia/Integration.php

ShareHead.phpShare lazy page head tags +39/-0

Share lazy page head tags

• Adds a lazy shared prop that stays off partial-reload responses.

src/head/src/Inertia/ShareHead.php

UniqueHeadGateway.phpDeduplicate SSR head tags +104/-0

Deduplicate SSR head tags

• Returns an immutable replacement response, preserving non-Head tags, body, and gateway capabilities.

src/head/src/Inertia/UniqueHeadGateway.php

RoutingMacros.phpDeclare withHead parameters +80/-0

Declare withHead parameters

• Defines named routing parameters without executing runtime macros.

src/head/src/PHPStan/RoutingMacros.php

WithHeadExtension.phpRecognize routing macros in PHPStan +65/-0

Recognize routing macros in PHPStan

• Discovers withHead and supplies receiver-aware return types across routing surfaces.

src/head/src/PHPStan/WithHeadExtension.php

WithHeadMethodReflection.phpPreserve routing method reflection +155/-0

Preserve routing method reflection

• Adapts reflected parameters and return types for withHead.

src/head/src/PHPStan/WithHeadMethodReflection.php

HeadRenderer.phpRender Blade and Inertia elements +164/-0

Render Blade and Inertia elements

• Orders output, separates globals, assigns page ownership, and removes duplicate SSR tags.

src/head/src/Rendering/HeadRenderer.php

ResolvedHead.phpExpose render-time metadata +169/-0

Expose render-time metadata

• Supplies title, description, canonical URL, request, and schema validation to builders.

src/head/src/Rendering/ResolvedHead.php

TagRenderer.phpSafely format head tags +221/-0

Safely format head tags

• Escapes attributes and renders tags with deterministic ownership keys and script-safe JSON-LD.

src/head/src/Rendering/TagRenderer.php

HeadRouteMacros.phpAdd typed withHead macros +462/-0

Add typed withHead macros

• Registers named-argument metadata methods for routes, groups, and resources.

src/head/src/Routing/HeadRouteMacros.php

RouteAttributeParser.phpApply route metadata layers +192/-0

Apply route metadata layers

• Stores cache-friendly attributes and applies ordered layers via registered builders.

src/head/src/Routing/RouteAttributeParser.php

Article.phpBuild Article schema +62/-0

Build Article schema

• Adds headline, author, dates, description, and image fields.

src/head/src/Schema/Article.php

BlogPosting.phpBuild BlogPosting schema +12/-0

Build BlogPosting schema

• Specializes article metadata as a blog posting.

src/head/src/Schema/BlogPosting.php

Brand.phpBuild Brand schema +35/-0

Build Brand schema

• Adds name, URL, and logo fields.

src/head/src/Schema/Brand.php

Breadcrumbs.phpBuild breadcrumb schema +43/-0

Build breadcrumb schema

• Creates individual or bulk ordered breadcrumb items.

src/head/src/Schema/Breadcrumbs.php

Event.phpBuild Event schema +70/-0

Build Event schema

• Adds descriptions, dates, organizers, and offers.

src/head/src/Schema/Event.php

Faq.phpBuild FAQ schema +45/-0

Build FAQ schema

• Creates single or bulk questions and answers.

src/head/src/Schema/Faq.php

GenericSchemaObject.phpSupport generic schema types +9/-0

Support generic schema types

• Allows schema.org types without dedicated classes.

src/head/src/Schema/GenericSchemaObject.php

Offer.phpBuild Offer schema +36/-0

Build Offer schema

• Adds price, currency, and availability.

src/head/src/Schema/Offer.php

Organization.phpBuild Organization schema +35/-0

Build Organization schema

• Adds name, URL, and logo.

src/head/src/Schema/Organization.php

Person.phpBuild Person schema +27/-0

Build Person schema

• Adds name and URL.

src/head/src/Schema/Person.php

Product.phpBuild Product schema +63/-0

Build Product schema

• Adds descriptions, images, SKU, brand, and offers.

src/head/src/Schema/Product.php

SchemaFactory.phpConstruct registered schemas +90/-0

Construct registered schemas

• Maps built-in and custom names to fresh objects with a generic fallback.

src/head/src/Schema/SchemaFactory.php

SchemaObject.phpDefine extensible schema objects +147/-0

Define extensible schema objects

• Coerces nested fields and dates, caches inferred types, and serializes JSON-LD data.

src/head/src/Schema/SchemaObject.php

SchemaValidator.phpValidate structured data +68/-0

Validate structured data

• Rejects invalid schemas outside production and logs production warnings.

src/head/src/Schema/SchemaValidator.php

WebPage.phpBuild WebPage schema +35/-0

Build WebPage schema

• Adds page name, description, and URL.

src/head/src/Schema/WebPage.php

WebSite.phpBuild WebSite schema +27/-0

Build WebSite schema

• Adds website name and URL.

src/head/src/Schema/WebSite.php

SchemaType.phpDeclare explicit schema types +18/-0

Declare explicit schema types

• Adds a class attribute for schema.org type names.

src/head/src/SchemaType.php

TagRegistry.phpRegister metadata builders +120/-0

Register metadata builders

• Orders built-ins, validates extensions, and caches route-attribute lookup.

src/head/src/TagRegistry.php

AlternateLinks.phpRender localized alternates +113/-0

Render localized alternates

• Builds and merges locale-to-URL links.

src/head/src/Tags/AlternateLinks.php

Canonical.phpNormalize canonical URLs +216/-0

Normalize canonical URLs

• Resolves explicit and automatic URLs while preserving query strings and fragments.

src/head/src/Tags/Canonical.php

Description.phpRender descriptions +33/-0

Render descriptions

• Emits layered description meta tags.

src/head/src/Tags/Description.php

FeedLinks.phpRender feed links +193/-0

Render feed links

• Builds repeatable syndication links with titles and types.

src/head/src/Tags/FeedLinks.php

GenericLinks.phpBuild generic links +263/-0

Build generic links

• Supports icons and manifests; later manifests replace inherited ones.

src/head/src/Tags/GenericLinks.php

GroupedTagBuilder.phpStandardize grouped defaults +17/-0

Standardize grouped defaults

• Represents absent tag collections as empty arrays.

src/head/src/Tags/GroupedTagBuilder.php

MetaTags.phpBuild keyed meta tags +228/-0

Build keyed meta tags

• Merges name, property, and media-specific definitions.

src/head/src/Tags/MetaTags.php

OpenGraph.phpBuild Open Graph metadata +488/-0

Build Open Graph metadata

• Tracks independent image, video, and audio defaults and document fallbacks.

src/head/src/Tags/OpenGraph.php

PaginationLinks.phpBuild pagination links +114/-0

Build pagination links

• Defines previous and next document links.

src/head/src/Tags/PaginationLinks.php

PerformanceLinks.phpBuild resource hints +438/-0

Build resource hints

• Supports preload, prefetch, preconnect, and DNS prefetch with asset detection.

src/head/src/Tags/PerformanceLinks.php

Robots.phpRender robots rules +87/-0

Render robots rules

• Normalizes string and enum directives.

src/head/src/Tags/Robots.php

Schemas.phpCollect structured data +159/-0

Collect structured data

• Merges and deduplicates schema objects or arrays for JSON-LD rendering.

src/head/src/Tags/Schemas.php

StringTagBuilder.phpShare scalar-tag layering +76/-0

Share scalar-tag layering

• Provides overlay and serialization for single-string builders.

src/head/src/Tags/StringTagBuilder.php

TagBuilder.phpDefine tag-builder contract +165/-0

Define tag-builder contract

• Establishes route, overlay, defaults, array, and HTML extension points.

src/head/src/Tags/TagBuilder.php

Title.phpBuild layered titles +142/-0

Build layered titles

• Supports inherited affixes, exact titles, and literal zero.

src/head/src/Tags/Title.php

Twitter.phpBuild Twitter cards +263/-0

Build Twitter cards

• Renders cards and images with document and Open Graph fallbacks.

src/head/src/Tags/Twitter.php

Exceptions.phpType the exception observer facade +1/-0

Type the exception observer facade

• Declares beforeResponding on the Exceptions facade.

src/support/src/Facades/Exceptions.php

Bug fix (1) +2 / -2
TestResponse.phpFix invalid JSON assertions +2/-2

Fix invalid JSON assertions

• Avoids accessing a missing exception property on plain Symfony responses.

src/testing/src/TestResponse.php

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

Record Head port plan

• Documents architecture, integration choices, implementation, and validation.

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

documentation.mdList the Head guide +1/-0

List the Head guide

• Adds Head to documentation navigation.

src/docs/documentation.md

errors.mdDocument response observers +18/-0

Document response observers

• Explains beforeResponding, callback ordering, and worker lifetime.

src/docs/errors.md

head.mdAdd Hypervel Head guide +793/-0

Add Hypervel Head guide

• Covers metadata, routes, schema, safe runtime configuration, Blade, Inertia, and SSR.

src/docs/head.md

inertia.mdLink Inertia to Head guidance +2/-0

Link Inertia to Head guidance

• Points Inertia users to Head integration instructions.

src/docs/inertia.md

porting-from-laravel.mdExplain worker-safe Head configuration +6/-0

Explain worker-safe Head configuration

• Advises moving request-dependent Laravel configuration to runtime resolvers.

src/docs/porting-from-laravel.md

LICENSE.mdInclude package license +23/-0

Include package license

• Adds license terms for the Head package.

src/head/LICENSE.md

README.mdIntroduce standalone Head package +12/-0

Introduce standalone Head package

• Provides package orientation and documentation links.

src/head/README.md

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

Register Head in the monorepo

• Adds package replacement, autoloading, facade alias, and provider discovery.

composer.json

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

Load Head analysis support

• Includes the Head PHPStan extension for source analysis.

phpstan.neon.dist

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

Load Head type-analysis support

• Includes the Head extension in type-test analysis.

phpstan.types.neon.dist

composer.jsonDefine hypervel/head package +73/-0

Define hypervel/head package

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

src/head/composer.json

extension.neonRegister routing PHPStan extension +5/-0

Register routing PHPStan extension

• Wires method reflection for withHead.

src/head/extension.neon

AfterEachTestSubscriber.phpClean Head test state +11/-0

Clean Head test state

• Flushes static canonical configuration, schema caches, and macros after tests.

src/testing/src/PHPUnit/AfterEachTestSubscriber.php

FoundationExceptionsHandlerTest.phpTest response observer ordering +39/-0

Test response observer ordering

• Verifies response identity, callback arguments, ordering, and ignored returns.

tests/Foundation/FoundationExceptionsHandlerTest.php

ArchTest.phpEnforce Head source conventions +82/-0

Enforce Head source conventions

• Checks strict typing and prohibited debugging and environment calls.

tests/Head/ArchTest.php

CoroutineIsolationTest.phpTest concurrent isolation +156/-0

Test concurrent isolation

• Covers independent metadata, fresh builders, and copied contexts.

tests/Head/CoroutineIsolationTest.php

ErrorPagesTest.phpTest error metadata +166/-0

Test error metadata

• Checks precedence, callbacks, Blade errors, and actual Inertia exception statuses.

tests/Head/Feature/ErrorPagesTest.php

HeadBuilderTest.phpTest fluent builders +100/-0

Test fluent builders

• Covers conditional definitions and registered schema factories.

tests/Head/Feature/HeadBuilderTest.php

HeadDataTest.phpTest builder extensions +72/-0

Test builder extensions

• Verifies custom route attributes and conflicting-registration errors.

tests/Head/Feature/HeadDataTest.php

HeadManagerTest.phpTest manager layers +161/-0

Test manager layers

• Covers runtime resolvers, precedence, status, and scoped flush.

tests/Head/Feature/HeadManagerTest.php

HeadServiceProviderTest.phpTest provider wiring +87/-0

Test provider wiring

• Checks service registration, Blade, optional Inertia, and early handler resolution.

tests/Head/Feature/HeadServiceProviderTest.php

InertiaCustomPropTest.phpTest custom shared prop +39/-0

Test custom shared prop

• Checks that tags use the configured Inertia prop.

tests/Head/Feature/InertiaCustomPropTest.php

InertiaTest.phpTest Inertia and SSR +444/-0

Test Inertia and SSR

• Exercises lazy props, ownership, templates, SSR deduplication, and gateway capabilities.

tests/Head/Feature/InertiaTest.php

HeadRendererTest.phpTest rendering ownership +148/-0

Test rendering ownership

• Checks ordering, globals, arrays, deduplication, and distinct title keys.

tests/Head/Feature/Rendering/HeadRendererTest.php

TagRendererTest.phpTest safe HTML rendering +95/-0

Test safe HTML rendering

• Verifies attribute validation and JSON-LD keys and encoding.

tests/Head/Feature/Rendering/TagRendererTest.php

RouteHeadTest.phpTest route metadata cascades +221/-0

Test route metadata cascades

• Covers groups, routes, resources, supported shapes, and invalid inputs.

tests/Head/Feature/Routing/RouteHeadTest.php

WithHeadTest.phpTest routing macros +113/-0

Test routing macros

• Checks group positions, resources, extensions, and named-argument rejection.

tests/Head/Feature/Routing/WithHeadTest.php

SchemaTest.phpTest schema APIs +215/-0

Test schema APIs

• Covers types, nesting, JSON-LD escaping, availability, and validation.

tests/Head/Feature/Schema/SchemaTest.php

CanonicalTest.phpTest canonical edge cases +129/-0

Test canonical edge cases

• Covers URL components, base paths, console origins, and runtime resolution.

tests/Head/Feature/Tags/CanonicalTest.php

GenericLinksTest.phpTest links and manifests +96/-0

Test links and manifests

• Checks aliases, safe attributes, repeatable icons, and manifest replacement.

tests/Head/Feature/Tags/GenericLinksTest.php

MetaTagsTest.phpTest meta tags +131/-0

Test meta tags

• Checks aliases, overlay keys, media values, and array conversion.

tests/Head/Feature/Tags/MetaTagsTest.php

OpenGraphTest.phpTest Open Graph fallbacks +131/-0

Test Open Graph fallbacks

• Checks independent media layering and literal zero values.

tests/Head/Feature/Tags/OpenGraphTest.php

PerformanceLinksTest.phpTest resource hints +69/-0

Test resource hints

• Checks asset detection, explicit types, and unknown assets.

tests/Head/Feature/Tags/PerformanceLinksTest.php

PwaTest.phpTest PWA helper +31/-0

Test PWA helper

• Checks generated PWA head tags.

tests/Head/Feature/Tags/PwaTest.php

RobotsTest.phpTest robots rendering +70/-0

Test robots rendering

• Checks strings, enums, convenience methods, and routes.

tests/Head/Feature/Tags/RobotsTest.php

ThemeColorTest.phpTest theme colors +64/-0

Test theme colors

• Checks media queries, routes, and Inertia keys.

tests/Head/Feature/Tags/ThemeColorTest.php

TitleTest.phpTest title layering +41/-0

Test title layering

• Checks suffixes, exact titles, and literal zero.

tests/Head/Feature/Tags/TitleTest.php

TwitterTest.phpTest Twitter fallbacks +91/-0

Test Twitter fallbacks

• Checks images, document fallbacks, and zero-valued properties.

tests/Head/Feature/Tags/TwitterTest.php

ConflictingRouteAttribute.phpFixture for conflicting attributes +47/-0

Fixture for conflicting attributes

• Supplies a builder with a duplicate route key.

tests/Head/Fixtures/ConflictingRouteAttribute.php

JobPosting.phpFixture for custom schema +29/-0

Fixture for custom schema

• Supplies a registered schema type.

tests/Head/Fixtures/JobPosting.php

ReadingTime.phpFixture for custom tags +86/-0

Fixture for custom tags

• Supplies a tag builder with route attributes.

tests/Head/Fixtures/ReadingTime.php

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

Fixture for early kernel resolution

• Exercises middleware installation after kernel resolution.

tests/Head/Fixtures/ResolvesHttpKernelEarly.php

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

Fixture for directive rendering

• Provides an Inertia Blade root template.

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

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

Fixture for Inertia-first rendering

• Places Inertia head before @head to exercise SSR deduplication.

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

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

Fixture for nested rendering

• Tests head ownership outside the root view scope.

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

app.blade.phpStandard Inertia root fixture +10/-0

Standard Inertia root fixture

• Provides an initial Inertia document template.

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

404.blade.phpError view fixture +9/-0

Error view fixture

• Exercises Blade error-page head rendering.

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

nested-head.blade.phpNested head fixture +1/-0

Nested head fixture

• Renders @head from a nested Blade view.

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

TestCase.phpSet up Head tests +36/-0

Set up Head tests

• Registers package providers and fixtures.

tests/Head/TestCase.php

TestResponseTest.phpRegress malformed-response JSON +11/-0

Regress malformed-response JSON

• Checks the intended assertion and stored-exception behavior.

tests/Testing/TestResponseTest.php

Facades.phpType-check Head facades +38/-0

Type-check Head facades

• Asserts callback and generic types.

types/Head/Facades.php

Route.phpType-check withHead calls +89/-0

Type-check withHead calls

• Asserts named parameters and return types across routing surfaces.

types/Head/Route.php

Comment thread src/head/src/Schema/SchemaObject.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/Tags/PaginationLinks.php Outdated
@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 (5) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Self-rendered error pages miss error metadata 🐞 Bug ≡ Correctness
Description
HeadServiceProvider captures status in a renderable callback that self-rendering exceptions
bypass, while its beforeResponding callback runs only after their response has been built. When an
exception's render() or toResponse() renders a view containing @head, that view resolves
metadata with a null status before the later callback can supply its actual status.
Code

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

+            $handler->renderable(function (Throwable $exception, Request $request) use ($head): void {
+                $head->status(
+                    $exception instanceof HttpExceptionInterface ? $exception->getStatusCode() : 500
Evidence
The handler returns from both self-rendering branches before renderViaCallbacks;
beforeResponding runs in finalization after response construction. Head reads the status during
resolution and only applies error definitions when it is non-null.

src/foundation/src/Exceptions/Handler.php[814-835]
src/foundation/src/Exceptions/Handler.php[847-863]
src/head/src/HeadServiceProvider.php[66-78]
src/head/src/HeadManager.php[296-307]

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

## Issue description
Exceptions that render their own view bypass Head's early status callback, and the response observer runs too late for `@head` in that view.
## Fix Focus Areas
- src/head/src/HeadServiceProvider.php[64-79]
- src/foundation/src/Exceptions/Handler.php[814-833]
## Recommended Fix
Provide an early status signal for the exception's own rendering paths, while retaining the prepared-response observer for views rendered later during response customization. Cover a self-rendered Blade view and a `Responsable` view.

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



Remediation recommended

2. Custom error pages can get 500 metadata 🐞 Bug ≡ Correctness
Description
HeadServiceProvider sets status to 500 for every non-HTTP exception before later renderable
callbacks can determine the response status. If a later application callback handles such an
exception by rendering a 404 view containing @head, that view uses 500 metadata, and the
prepared-response observer corrects the stored status only after rendering.
Code

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

+            $handler->renderable(function (Throwable $exception, Request $request) use ($head): void {
+                $head->status(
+                    $exception instanceof HttpExceptionInterface ? $exception->getStatusCode() : 500
+                );
Evidence
The early callback uses the exception type rather than the response, and the handler invokes
renderables in order before its response-observer callbacks. Head resolves error definitions from
the status already stored when the view renders.

src/head/src/HeadServiceProvider.php[66-78]
src/foundation/src/Exceptions/Handler.php[959-974]
src/foundation/src/Exceptions/Handler.php[847-863]
src/head/src/HeadManager.php[298-307]

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 early exception callback guesses 500 for non-HTTP exceptions even when a later renderable constructs an error view with another response status.
## Fix Focus Areas
- src/head/src/HeadServiceProvider.php[64-79]
- src/foundation/src/Exceptions/Handler.php[959-974]
## Recommended Fix
Make the actual status available before custom renderables materialize their views, or provide a supported way for those views to receive the response status. Add coverage for a non-HTTP exception whose renderable returns a 404 view containing `@head`.

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


3. Custom exception handlers can fail at boot ✓ Resolved
Description
HeadServiceProvider calls renderable() and beforeResponding() on the service bound to
ExceptionHandler without checking whether it is the framework handler. An application handler
implementing only that contract has neither method, so resolving the handler after Head boots raises
an undefined-method error.
Code

src/head/src/HeadServiceProvider.php[R66-70]

+        $this->callAfterResolving(ExceptionHandler::class, function (object $handler): void {
+            $head = $this->app->make(HeadManager::class);
+
+            /** @var Handler $handler */
+            $handler->renderable(function (Throwable $exception, Request $request) use ($head): void {
Evidence
The contract defines neither callback-registration method, whereas the provider invokes both on the
contract binding without a runtime guard.

src/head/src/HeadServiceProvider.php[64-79]
src/contracts/src/Debug/ExceptionHandler.php[16-60]

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

## Issue description
Head assumes every exception-handler implementation exposes callback-registration methods absent from the handler contract.
## Fix Focus Areas
- src/head/src/HeadServiceProvider.php[64-79]
## Recommended Fix
Register status callbacks only when the bound handler supports the required methods, or introduce a supported contract for those hooks. Test Head with a handler implementing only `ExceptionHandler`.

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


4. Inferred schema types contain spaces ✓ Resolved
Description
SchemaObject::typeFromAttribute() uses Str::headline() as its fallback, turning an unannotated
class such as JobPosting into the JSON-LD type Job Posting. A custom schema class constructed
without #[SchemaType] passes the current nonempty-type validation and emits that value instead of
a schema type matching its class name.
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 calls a formatter that separates capitalized words with spaces; the factory permits
custom schema subclasses and validation only requires a nonempty type.

src/head/src/Schema/SchemaObject.php[126-138]
src/support/src/Str.php[1324-1335]
src/head/src/Schema/SchemaFactory.php[70-79]
src/head/src/Schema/SchemaValidator.php[27-32]

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 schema type is formatted as a human-readable headline rather than a schema.org-style class name.
## Fix Focus Areas
- src/head/src/Schema/SchemaObject.php[126-138]
## Recommended Fix
Use the short class name unchanged for inferred types, retaining `#[SchemaType]` as the explicit override. Test an unannotated multiword custom schema class through JSON-LD rendering.

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


View medium (4)
5. Advertised pagination routes always fail ✓ Resolved
Description
PaginationLinks inherits the default route-attribute key and parser, which advertises pagination
but returns null for every value. Supplying that key in route head metadata reaches
RouteAttributeParser::fill() and throws an invalid-value exception, even though the registry lists
it among accepted attributes.
Code

src/head/src/Tags/PaginationLinks.php[R28-31]

+    public static function key(): string
+    {
+        return 'pagination';
+    }
Evidence
The inherited route key comes from key(), but the inherited parser always returns null; the route
parser rejects that result.

src/head/src/Tags/PaginationLinks.php[24-39]
src/head/src/Tags/TagBuilder.php[45-61]
src/head/src/Routing/RouteAttributeParser.php[134-152]
src/head/src/TagRegistry.php[78-94]

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 pagination builder advertises a route attribute that it cannot parse.
## Fix Focus Areas
- src/head/src/Tags/PaginationLinks.php[24-39]
- src/head/src/Tags/TagBuilder.php[45-61]
## Recommended Fix
Either implement parsing for a documented pagination route-attribute shape or override `routeAttributeKeys()` to return no route keys. Cover the registry's advertised key against an actual route definition.

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


6. Malformed alternates produce invalid locales 🐞 Bug ≡ Correctness
Description
AlternateLinks::fromRouteAttribute() casts each array key to a locale and silently skips entries
whose URL is not a string. A list of alternate URLs therefore renders hreflang="0" and
hreflang="1", while a locale mapped to a non-string URL disappears rather than failing
route-metadata validation.
Code

src/head/src/Tags/AlternateLinks.php[R51-55]

+        foreach ($value as $locale => $href) {
+            if (is_string($href)) {
+                $links->link((string) $locale, $href);
+            }
+        }
Evidence
The parser accepts any array, casts keys to strings, and skips invalid values; the renderer uses
those keys as hreflang, while route validation rejects only a null builder.

src/head/src/Tags/AlternateLinks.php[42-58]
src/head/src/Tags/AlternateLinks.php[98-112]
src/head/src/Routing/RouteAttributeParser.php[134-152]

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

## Issue description
Alternate route metadata accepts integer locale keys and drops malformed URL entries, producing incorrect or incomplete link tags.
## Fix Focus Areas
- src/head/src/Tags/AlternateLinks.php[42-58]
- src/head/src/Tags/AlternateLinks.php[98-112]
## Recommended Fix
Require string locale keys and string URLs for every entry; return null for an invalid map so the route parser reports its invalid-value error. Add list-key and non-string-URL cases.

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


7. SSR path exclusions crash custom-gateway apps 🐞 Bug ☼ Reliability
Description
UniqueHeadGateway unconditionally implements ExcludesSsrPaths, DisablesSsr,
ConfiguresSsrRequests, and HasHealthCheck, making callers’ instanceof checks pass even when
the wrapped gateway lacks those capabilities and the forwarding methods throw LogicException. With
a custom gateway that lacks path-exclusion support, setting $withoutSsr makes Inertia’s
Middleware::handle call except() and fail on requests once Head is installed;
inertia:check-ssr can likewise throw instead of printing its unsupported-health-check message.
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
The decorator declares the optional capability interfaces unconditionally, but its forwarding
methods throw when the wrapped gateway does not implement them. Inertia middleware checks
instanceof ExcludesSsrPaths before calling except(), and CheckSsr also uses an instanceof
capability check, so both guards now lead to throwing methods rather than detecting unsupported
operations.

src/head/src/Inertia/UniqueHeadGateway.php[19-103]
src/inertia/src/Middleware.php[159-163]
src/inertia/src/Commands/CheckSsr.php[28-41]
src/head/src/Inertia/Integration.php[59-66]
src/head/src/Inertia/UniqueHeadGateway.php[19-28]
src/head/src/Inertia/UniqueHeadGateway.php[72-78]
src/inertia/src/Middleware.php[157-165]
src/head/src/Inertia/Integration.php[59-64]

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` makes optional SSR capability checks succeed even when its wrapped gateway does not support those capabilities. Middleware exclusions and the SSR health-check command can therefore invoke methods that throw instead of following their unsupported-capability paths.
## Fix Focus Areas
- src/head/src/Inertia/UniqueHeadGateway.php[19-103]
- src/head/src/Inertia/Integration.php[59-66]
## Recommended Fix
Preserve capability detection for the wrapped gateway. Prefer a base decorator that implements only `Gateway`, with `Integration` selecting or composing capability-specific decorators so each `instanceof` check remains accurate; alternatively, adapt callers’ checks to inspect the underlying gateway. If retaining the current decorator, make `except()` a no-op when the inner gateway lacks path-exclusion support to preserve middleware’s prior skip behavior. Add tests using a minimal custom gateway with `$withoutSsr` configured, and cover `inertia:check-ssr` when health checks are unsupported.

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


8. Route feed given as a URL string breaks pages ✓ Resolved
Description
Every withHead macro, plus the PHPStan declaration, types $feed as string|array|null, but
FeedLinks::fromRouteAttribute returns null for any non-array value. RouteAttributeParser::fill
then throws InvalidArgumentException, so ->withHead(feed: '/feed.xml') registers fine and then
fails on every render of that route's head, both Blade and the Inertia shared prop.
Code

src/head/src/Tags/FeedLinks.php[R55-59]

+    public static function fromRouteAttribute(string $key, mixed $value): ?self
+    {
+        if ($key !== 'feed' || ! is_array($value)) {
+            return null;
+        }
Evidence
The macro signatures accept a string feed, but the builder rejects it, and the parser turns that
rejection into an exception on each resolve.

src/head/src/Routing/HeadRouteMacros.php[79-79]
src/head/src/Routing/HeadRouteMacros.php[115-115]
src/head/src/Tags/FeedLinks.php[55-70]
src/head/src/HeadManager.php[290-294]

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

## Issue description
`withHead(feed: '...')` passes type checks, but FeedLinks::fromRouteAttribute rejects strings, so every render throws InvalidArgumentException.
## Fix Focus Areas
- src/head/src/Tags/FeedLinks.php[55-70]
- src/head/src/Routing/HeadRouteMacros.php[79-115]
- src/head/src/PHPStan/RoutingMacros.php[32-68]
- types/Head/Route.php
## Recommended Fix
Narrow the `$feed` parameter type and docblocks to `array|null` in all macros, PHPStan declarations and type stubs, so the mistake shows up at the call site. Alternatively, support a string by treating it as the href with a sensible default title. Add a test for the chosen behaviour.

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



Informational

9. Options-only canonical drops the inherited URL 🐞 Bug ≡ Correctness
Description
Canonical::make() switches to 'auto' mode whenever the URL is null, and overlayOn() keeps the
overlay's mode while inheriting the base's url. Because render() ignores url in auto mode, the
inherited URL is dead state. So a page or route that only sets forceHttps/trailingSlash (e.g.
canonical(trailingSlash: true) over a default canonical('https://site.test/')) renders the
current request URL instead of the configured canonical.
Code

src/head/src/Tags/Canonical.php[R75-80]

+        return new static(
+            $this->mode,
+            $this->url ?? $base->url,
+            $this->forceHttps ?? $base->forceHttps,
+            $this->trailingSlash ?? $base->trailingSlash,
+        );
Evidence
The mode always comes from the overlay, but the URL falls back to the base, and render() branches on
mode alone, so the inherited URL is never used.

src/head/src/Tags/Canonical.php[44-47]
src/head/src/Tags/Canonical.php[69-81]
src/head/src/Tags/Canonical.php[94-107]

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

## Issue description
In Canonical::overlayOn the mode always comes from the overlay while the URL comes from the base, so an options-only overlay replaces an explicit base URL with the request URL.
## Fix Focus Areas
- src/head/src/Tags/Canonical.php[44-81]
## Recommended Fix
Track whether auto mode was requested explicitly, as opposed to only options being passed. When the overlay has no URL and auto was not requested, keep the base's mode and URL and overlay only the options. Otherwise, stop inheriting `url` so the intent is clear. Add a test for defaults canonical(url) plus page canonical(trailingSlash: true).

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


10. Same-key meta tags collapse during Inertia visits ✓ Resolved
Description
MetaTags::tagKey() tells name= and property= entries apart, but inertiaKey() uses only the
meta key, and HeadRenderer::uniqueInertiaElements deduplicates on that data-inertia value. When
a page defines both forms of one key (e.g. meta('og:title', ..., property: false) plus Open
Graph's og:title property), Inertia keeps only one of the two tags. Blade output keeps both.
Code

src/head/src/Tags/MetaTags.php[R184-191]

+    }
+
+    /**
+     * Get the stable Inertia ownership key for a meta tag.
+     *
+     * @param MetaAttributes $meta
+     */
+    protected function inertiaKey(array $meta): string
Evidence
The storage identity includes the attribute, but the Inertia ownership key does not, so two distinct
stored tags share one dedup key.

src/head/src/Tags/MetaTags.php[176-191]
src/head/src/Rendering/HeadRenderer.php[96-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
MetaTags::inertiaKey omits the HTML attribute (name/property), so distinct tags collide in Inertia deduplication.
## Fix Focus Areas
- src/head/src/Tags/MetaTags.php[176-191]
## Recommended Fix
Build the inertia key from the same identity as tagKey(), i.e. attribute plus key plus media, so each stored tag gets a distinct stable ownership key. Add a test that defines both name= and property= forms of one key.

ⓘ 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/HeadServiceProvider.php Outdated
Comment thread src/head/src/HeadServiceProvider.php Outdated
Comment thread src/head/src/Schema/SchemaObject.php
Comment thread src/head/src/Tags/PaginationLinks.php
Comment thread src/head/src/Tags/AlternateLinks.php
Comment thread src/head/src/Inertia/UniqueHeadGateway.php
Comment thread src/head/src/Tags/FeedLinks.php
Comment thread src/head/src/Tags/Canonical.php
Comment thread src/head/src/Tags/MetaTags.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
52 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