bjuppa/metatagbag
Metatag Bag is a Laravel package for managing and rendering SEO meta tags from a fluent PHP API. Define titles, descriptions, Open Graph, Twitter cards, and canonical URLs once, then output clean tags in your Blade views with an extensible, testable configuration.
This release (v2.0.1) is a platform-support change only — no API changes — but it does move the compatibility floor:
| Package | Laravel | PHP |
|---|---|---|
| v2.0.1 | 11.x / 12.x / 13.x | check composer.json (^8.2+ likely) |
| v2.0.0 | 9.x / 10.x / 11.x / 12.x | — |
^2.0.0 (pin explicitly: "bjuppa/metatagbag": "2.0.0") or upgrade the framework. Do not let composer update surprise you: run composer why-not bjuppa/metatagbag 2.0.1 to see exactly which constraint is holding you back.composer require bjuppa/laravel-metatagbag, then php artisan vendor:publish --tag=metatagbag-config to get config/metatagbag.php.default_tags (applied to every response), tag_renderers (how each tag type is turned into HTML), and the Blade directive name. Read this before reading the README examples — the defaults explain most "why is this tag here?" questions.default_tags (title, description, og:site_name), then override per controller action for the page-specific values. That two-tier model is the whole point of the package and is the fastest way to understand it.Per-request tags in the controller. The facade is the primary entry point; tags added here override or append to the configured defaults, and are rendered by the Blade directive in your layout <head>:
public function show(Post $post)
{
MetaTag::tag('description', Str::limit(strip_tags($post->body), 155));
MetaTag::tag('title', $post->title);
MetaTag::openGraph()
->tag('type', 'article')
->tag('title', $post->title)
->tag('image', $post->heroUrl());
MetaTag::twitter()
->tag('card', 'summary_large_image');
return view('posts.show', compact('post'));
}
tag() accepts an array as well, which keeps multi-tag blocks from turning into a wall of fluent calls.
Registered bags for reusable sets. Rather than duplicating SEO blocks, register a named bag (e.g. from a service provider or a ViewComposer) and pull the same set into multiple views. This is the cleanest way to keep product/blog/legal pages consistent without inheritance gymnastics.
Layout-level rendering. Render once in the layout with the package's Blade directive (default name is configurable under the directive key). If you have multiple layouts (app, marketing, error pages), make sure each renders it — meta tags silently disappearing on the marketing layout is the classic first-week bug.
Model-driven tags. A HasMetaTags trait or an observer that hydrates tags on retrieved works well for content types where editors care about SEO, but prefer computing them in a dedicated SeoPresenter/MetaBuilder class over stuffing logic into the model. You will want to unit-test that builder without booting a full request.
Integration with view composers. For anything route-dependent (canonical URLs, og:url, paginated rel=prev/next), do it in a view composer bound to the layout, not in every controller. Composers run after the controller, so controller-set tags can still win if your composer only fills gaps.
composer.json has "bjuppa/laravel-metatagbag": "^2.0" on a Laravel 10 app, Composer will correctly refuse 2.0.1 — but a CI image with a floating platform or a composer update --ignore-platform-reqs habit can produce confusing failures. Pin 2.0.0 deliberately and document why in a comment.og: tags appear in production. Reset/flush the bag in a middleware terminate() or a tearDown() hook; add a test that asserts a clean bag between requests.<meta name="description"> twice, the culprit is almost always a default plus a controller override, not the renderer. Decide early whether your convention is "defaults are fallbacks" (check-then-set) or "last write wins," and enforce it in one helper.Str::limit after stripping tags, and remember the renderer escapes — do not hand-build HTML strings and inject them.php artisan config:cache in deploy is mandatory, but it also means edits to default_tags require a re-cache. When production meta tags "ignore" your change while local works, this is the first thing to check.tag_renderers in the config rather than emitting raw markup in the Blade template. Keeping all <meta>/<link> output behind the renderers is what makes the package testable and lets you swap formats later.composer update bjuppa/laravel-metatagbag --with-dependencies, then smoke-test one rendered page for the presence of your default tags, Open Graph tags, and the canonical URL. There is no API surface to migrate, so a visual diff of the <head> is a sufficient regression test.How can I help you explore Laravel packages today?