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

Laravel Meta Laravel Package

f9webltd/laravel-meta

f9webltd/laravel-meta provides a simple way to manage SEO meta tags in Laravel. Attach metadata to Eloquent models, define reusable defaults and templates, and render dynamic titles, descriptions, Open Graph and Twitter tags for cleaner, more consistent pages.

View on GitHub
Deep Wiki
Context7

Getting Started

Check your runtime first — this is the whole story of 4.0.0. The package now requires PHP ^8.2 and Laravel ^11.0 || ^12.0 || ^13.0. If you are on Laravel 10 or PHP 8.1, stay on ^3.0 and treat 4.x as blocked until you upgrade the app. Verify with:

composer why-not f9webltd/laravel-meta ^4.0
php -v
php artisan --version

Once on a supported stack:

composer require f9webltd/laravel-meta

Service provider and facade/helper are auto-discovered — there is nothing to register by hand in Laravel 11+. Publish the config so defaults are versioned with your app rather than living in vendor/:

php artisan vendor:publish --tag=laravel-meta-config

Look at the config file first: that is where site-wide defaults (title suffix/separator, default description, default OG image, robots defaults) live. Set those once and most pages need zero code.

The first use case to wire up is a layout head partial. Put the render call in resources/views/layouts/app.blade.php (or your <x-head> component) exactly once, so every page inherits it:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    {!! meta()->render() !!}
</head>

Then override per page in the controller:

public function show(Product $product)
{
    meta()->setTitle($product->name)
        ->setDescription(Str::limit($product->summary, 155))
        ->setImage($product->hero_image_url)
        ->setCanonical(route('products.show', $product));

    return view('products.show', compact('product'));
}

That single pattern — defaults in config, overrides in the controller, one render in the layout — covers ~80% of day-to-day usage. Everything below is refinement.

Implementation Patterns

Defaults via middleware for whole sections. Rather than repeating the same six setters in every controller of a section, set section-level defaults in middleware and let controllers override only what differs:

// app/Http/Middleware/SetSectionMeta.php
public function handle(Request $request, Closure $next)
{
    meta()->setTitleSuffix(' — Acme')
        ->setRobots($request->is('admin/*') ? 'noindex,nofollow' : 'index,follow');

    return $next($request);
}

In Laravel 11+ register it in bootstrap/app.php (->withMiddleware(fn (Middleware $m) => $m->web(append: [...]))), not in a Kernel.php that no longer exists. Append it after StartSession but keep it in the web group so it runs before the controller.

CMS-driven meta through a model concern. When editors own the copy, put SEO columns on the model and hydrate in one place rather than in twenty controllers:

trait HasSeoMeta
{
    public function applySeoMeta(): void
    {
        meta()->setTitle($this->meta_title ?: $this->title)
            ->setDescription($this->meta_description)
            ->setCanonical($this->canonical_url ?: url()->current())
            ->setImage($this->og_image);
    }
}

Call it from a route-model-binding callback or a viewModel-style hook so it fires for every consumer of that resource.

View composer for cross-cutting tags. OG/Twitter defaults, an organisation JSON-LD blob, or a verification tag are better as a composer on your layout than as controller noise:

View::composer('layouts.app', fn ($view) => meta()->setProperty('og:site_name', config('app.name')));

Compose rather than concatenate. Keep description length logic in a helper (Str::limit($text, 155)) applied at the point of setting, not in Blade. Rendering-time truncation silently breaks on multibyte/HTML entity boundaries and makes the output untestable.

Test the head, not the package. Assert on rendered output in feature tests so refactors of the meta layer are safe:

$response->assertSee('<meta name="description" content="Acme widgets">', false);
$response->assertSee('<link rel="canonical" href="https://acme.test/products/1">', false);

Cache the fragment, not the page, when meta is static. If your layout renders identical meta for a large set of pages (category listings, tag archives), wrap the render in @cache / Cache::remember keyed on the route + locale. Do not cache when the meta depends on the authenticated user or the request host.

Localisation. Anything user-visible in the head — title suffix, description, OG locale — needs to follow your app()->getLocale() switch. Set the suffix inside a locale-aware middleware, or you will ship English suffixes on /de/ pages.

Gotchas and Tips

The 4.0.0 upgrade is a constraint change, not an API change. The release notes only mention the support matrix, so the practical upgrade is: bump the constraint, run composer update f9webltd/laravel-meta, and confirm your CI matrix actually tests on 8.2/8.3/8.4 and the Laravel version you claim. If you support two Laravel majors, widen to ^11.0|^12.0|^13.0 rather than pinning. Pin ^3.0 in any satellite package or internal library that still supports Laravel 10 — a shared dependency on ^4.0 will drag the whole monorepo forward.

Going forward, the package tracks Laravel's support policy. That means a future 5.x will drop Laravel 11 on Laravel's schedule, not yours. Budget for one constraint bump per year, and keep the upgrade isolated by depending on the package in a thin internal wrapper or a single Blade component rather than sprinkling meta() calls across 60 views. If you ever need to swap renderers, one file changes instead of the whole app.

Singleton state leaks under Octane and in long-running workers. A meta container is request-scoped state held in a singleton. Under Octane, tags set for request A can bleed into request B. If you run Octane, reset the instance on RequestReceived/RequestTerminated (or in a terminate()-based middleware). This is the single most common production-only bug with packages of this shape, and it manifests as SEO tags from the previous page appearing on the next — invisible in a standard FPM test suite.

Escape carefully with {!! !!}. The render call is unescaped by design. Anything sourced from user input (page titles from a form, product names, meta descriptions written by non-admins) must be escaped before it reaches a setter, or you have stored XSS inside <head>. Prefer e() at the call site or sanitise in the model accessor; do not "fix" it by escaping the whole rendered block, which will mangle your JSON-LD.

Order of execution matters more than order of setters. Meta set in a controller after the view has been rendered is lost. If you set it in a View::composer, remember composers fire before render — fine — but Response middleware that mutates the body runs later and will not see it. Keep all meta decisions at or before the controller.

Config caching will bite you. php artisan config:cache snapshots the published config. If you use env() inside the config file (tempting for OG image URLs), it will return null in production. Use config values only, and re-run config:clear after editing. Also clear the view cache if you have compiled the layout with the render call.

SPA and Livewire boundaries. With Inertia or a client-rendered SPA, only the initial server-rendered head is real. Client-side route changes need a separate client-side library; do not assume server-side meta survives navigation. In Livewire, meta set inside a component's mount() runs on the initial page load only, not on subsequent wire:navigate transitions — set it in the full-page controller instead.

Multibyte and entity handling. Str::limit counts characters, and search engines count pixels. A 155-character description containing &, " and emoji may render shorter or longer than expected, and an unescaped " in a setter will break the attribute. Sanitise once, in the model or a form request, and assert on output in a test rather than eyeballing <head> in devtools.

Debugging a missing tag. Work down the layers in this order: is the render call present in the layout actually in use (not a stale cached view)? does the config have the default? did a later middleware overwrite it? is the setter being called on the same container instance (facade vs. injected)? Dumping meta() immediately before render() resolves it in minutes; guessing wastes an hour.

Extension points to prefer over forking. Custom/arbitrary tags are usually expressible through the generic property/setter method — reach for that before subclassing the renderer. If you need genuinely new behaviour, bind your own implementation in a service provider against the package's contract rather than editing vendor/, and wrap the render call in your own Blade component so the substitution point is one line.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin