Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Head Laravel Package

laravel/head

Laravel Head offers a fluent API for managing your app's document head—titles, meta tags, Open Graph, canonical URLs, robots directives, performance hints, and structured data. Works with Blade, Livewire, and Inertia.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the render point first.

composer require laravel/head

The package is official but young (small install base, zero dependents), so treat the official docs as canonical and confirm exact method signatures before you build conventions on them. Skim CHANGELOG.md and the releases page before writing anything — on a package this new, minor bumps are where renames land.

The first thing to get right is not the API, it's the single render point. Head management only works if exactly one place in your base layout emits the collected tags:

{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    {{-- whatever the package's render call is: Head::render(), <x-head />, @head --}}
    {{ Head::render() }}

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

Then delete your hardcoded <title> and per-page <meta> blocks. If both the layout and a page emit a title you get two <title> tags and browsers/crawlers pick arbitrarily.

First use case: replace the {{-- <title>{{ $title ?? config('app.name') }}</title> --}} pattern in one layout, set a site-wide default at the layout level, and override per page from a controller:

public function show(Post $post)
{
    return view('posts.show', compact('post'))
        ->with('head', fn () => Head::title($post->title)
            ->description(Str::limit($post->excerpt, 155))
            ->canonical(route('posts.show', $post)));
}

Prove it end-to-end with curl -s https://your-app.test/posts/1 | grep -i '<title\|canonical' before rolling it across the app.

Implementation Patterns

Layered defaults, page-level overrides. Establish site-wide head (app name suffix, default OG image, Twitter card type, favicons, preconnects) once — in a base layout, a view composer, or a HeadServiceProvider — and let pages override individual keys. Decide early whether your version of the package merges or replaces on repeated calls: that single behaviour determines whether a page has to re-specify the OG image every time. Write one test that asserts it and keep it green across upgrades.

Presenter class per resource type, not inline chains. Head logic in controllers rots fast. Put it in a small class that knows how to describe a model:

final class PostHead
{
    public static function for(Post $post): Head
    {
        return Head::title($post->title)
            ->description(Str::limit(strip_tags($post->excerpt), 155))
            ->canonical(route('posts.show', $post))
            ->image($post->ogImageUrl() ?? asset('img/og-default.png'))
            ->type('article')
            ->publishedAt($post->published_at)
            ->jsonLd(ArticleSchema::for($post));
    }
}

Call it from the controller, a Blade component, an Inertia response, or a Livewire mount() — same code, one place to fix.

Structured data: use the built-in schemas where they fit. As of v0.2.2 the package ships an event schema helper (PR #13), alongside the article-style schemas. For events, conferences, webinars, and anything with a date/venue, prefer the first-party helper over hand-rolled JSON-LD — it keeps the shape consistent with the rest of the head chain and survives future fixes. Reserve manual JSON-LD for bespoke types (Product, FAQ, BreadcrumbList) that the package doesn't model yet.

public static function for(Event $event): Head
{
    return Head::title($event->name)
        ->description(Str::limit($event->summary, 155))
        ->canonical(route('events.show', $event))
        ->eventSchema($event); // v0.2.2+
}

Because a new schema type just landed, re-read the schema docs before hand-building one — the surface is still growing, and the built-in version will be tested against Google's Rich Results expectations where your hand-rolled array may not be.

Reusable Blade components for shared shapes. <x-seo.post :post="$post" /> keeps views tidy and gives you a single override surface when marketing wants to tweak card copy.

Canonical URLs need policy, not a default. Blindly canonicalising to the current URL means ?utm_source=, ?page=2, and filtered views all self-canonicalise and compete with each other. Canonicalise paginated/filtered pages to the clean base URL, and deliberately leave truly distinct filtered pages non-canonical.

Environment-aware directives. noindex for staging and for auth/checkout/thank-you routes belongs in a middleware or layout conditional, not copy-pasted. One if (! app()->isProduction()) beats fifty forgotten pages.

Inertia. Set head data server-side in the Inertia response for correct first paint and crawler-visible tags, and use the client-side head component for anything that changes on SPA navigation (route params, live title updates). Verify by navigating client-side and viewing source vs. inspecting — they diverge, and only one of them is what a crawler sees on first load.

Livewire. wire:navigate swaps the document without a full reload; confirm the package re-applies head data on navigation rather than leaving the previous page's title. Test with a direct visit and a navigation from another page.

Performance hints (preload, preconnect, dns-prefetch) are easy wins for fonts and CDN origins — but scope them per layout, not globally, or every page pays for assets only one page uses.

Test the rendered HTML, not the builder. HTTP tests asserting assertSee('<link rel="canonical"', false) and assertSee($post->title) catch duplicates and escaping bugs that unit-testing the fluent chain never will. Add a cheap assertion for substr_count($html, '<title') === 1.

Gotchas and Tips

  • Zero render point = silent no-op; two render points = duplicate tags. Both failures are invisible in the browser tab title, which uses the first one. Grep your layouts for the render call whenever head "isn't working".
  • Verify merge semantics before you rely on them. Whether a second title() call wins, is ignored, or appends decides your whole inheritance strategy. One test on day one saves a debugging session on day ninety.
  • Escaping and JSON-LD. Meta content should be escaped by the renderer, so never pass pre-escaped strings or raw HTML. For structured data, ensure </script> sequences in user-controlled strings can't break out — validate any JSON-LD that contains a model field a user can edit. This applies to the v0.2.2 event schema exactly as it does to your hand-rolled arrays: event names, locations, and descriptions are often user-submitted.
  • Full-page caching plus per-request head is a leak. Personalised titles, user-specific OG data, or flash-driven robots directives baked into a cached response will be served to the next visitor. Keep head out of cached fragments or fold the varying bits into the cache key.
  • Debug with raw source, not DevTools. The Elements panel shows post-JS DOM; curl, "View Page Source", or Http::get() in tinker shows what crawlers and social scrapers actually receive. Duplicate tags are obvious in one and invisible in the other.
  • Validate with real scrapers. Facebook Sharing Debugger, LinkedIn Post Inspector, and Google Rich Results Test each cache and each parse differently. A title that renders fine can still be truncated at 60 chars or rejected as invalid schema. New schema helpers (the v0.2.2 event type included) are worth running through Rich Results Test at least once on a real entity before you trust them.
  • Don't hit the database inside the head chain. OG images, authors, and categories referenced from PostHead::for() should come from already-eager-loaded relations; this runs on every render. Schema builders that accept a model are especially prone to lazy-loading relations.
  • Isolate yourself from API churn. With ~123 stars and no dependents, you are the QA team. Pin the version in composer.json, read the changelog on every bump, and route all head construction through a couple of presenter classes so a renamed method is a one-file change rather than a codebase-wide find-and-replace. v0.2.2 is additive, but it's a reminder that the schema API is still moving — assert on the rendered JSON-LD, not on a method name returning truthy.
  • Extension points worth knowing: whether the builder is macroable (add your own ->hreflang() or ->analytics() helpers), whether custom tags/raw snippets can be pushed, and whether a custom renderer or Blade directive exists. Custom macros are the cheapest way to encode house conventions without forking.
  • Add a lint check. A tiny test that crawls a handful of key routes and asserts each has exactly one title, one description, one canonical, and a valid absolute canonical URL catches the regression class this package exists to prevent.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect