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

Meta Tags Laravel Package

butschster/meta-tags

Laravel package for managing SEO meta tags with a fluent API. Define page titles, descriptions, canonical URLs, Open Graph and Twitter Card tags, and custom meta tags, with Blade directives, JSON-LD structured data, and per-page configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its config:

composer require butschster/meta-tags
php artisan vendor:publish --tag=meta-tags-config   # or the provider tag shown in the README

The service provider is auto-discovered on Laravel 5.5+. If you're on an older scaffold (or a package that disables discovery), register Butschster\MetaTags\MetaTagsServiceProvider in config/app.php and add the MetaTag facade alias.

Next, open your layout's <head> and render the collected tags once:

{{-- resources/views/layouts/app.blade.php --}}
<head>
    <meta charset="utf-8">
    {!! MetaTag::render() !!}
</head>

Then set tags from wherever you have the data — a controller, a view composer, or the Blade view itself:

// app/Http/Controllers/PostController.php
public function show(Post $post)
{
    MetaTag::set('title', $post->title);
    MetaTag::set('description', Str::limit(strip_tags($post->excerpt), 155));
    MetaTag::set('image', $post->cover->url());

    return view('posts.show', compact('post'));
}

Your first end-to-end use case is a blog/detail page: set title, description, and image on the controller action, render in the layout, and check the page source to confirm the OG/Twitter tags land where you expect. That single flow (defaults in layout → overrides in controller) is the pattern you'll repeat everywhere.

Where to look first:

  1. config/meta_tags.php after publishing — this is where site-wide defaults, OpenGraph/Twitter toggles, and separators live.
  2. The package's Blade view (usually publishable/overridable) if you need to change tag order or add wrappers.
  3. The MetaTag facade class if you need to know what set()/get()/render() actually return.

Implementation Patterns

Site-wide defaults in a base layout or middleware. Set og:site_name, twitter:site, default image, and robots once, then override per page. Doing it in a View::composer('layouts.*') or a SetDefaultMetaTags middleware keeps controllers clean and guarantees every response has sane values:

// app/Http/Middleware/DefaultMetaTags.php
public function handle($request, Closure $next)
{
    MetaTag::set('og:site_name', config('app.name'));
    MetaTag::set('twitter:card', 'summary_large_image');
    MetaTag::set('robots', 'index,follow');

    return $next($request);
}

Defaults first, overrides second. Because the package stores tags keyed by name, call the default-setting code before the per-action code, or use the config file's defaults rather than calling set() twice. Order of set() calls is your override mechanism — the last write wins for a given key.

Keep model formatting out of the controller. If a model has SEO concerns, add a small method or a dedicated presenter:

// app/Models/Post.php
public function metaTags(): array
{
    return [
        'title'       => $this->title,
        'description' => Str::limit(strip_tags($this->excerpt), 155),
        'image'       => $this->cover?->url(),
        'og:type'     => 'article',
    ];
}
foreach ($post->metaTags() as $name => $value) {
    MetaTag::set($name, $value);
}

Blade partial for page-specific tags. When the meta data is only known inside the view (e.g. inside a @foreach or after expensive eager loads), set at the top of the view rather than in the controller. It's still rendered before the layout's <head> because Blade compiles the child view first — but don't rely on this from components that render later in the body.

OpenGraph and Twitter as first-class. OG titles are usually not the same as the <title> (you often want og:title without the site suffix). Set both explicitly:

MetaTag::set('title', $post->title . ' — ' . config('app.name'));
MetaTag::set('og:title', $post->title);
MetaTag::set('og:description', $post->excerpt);
MetaTag::set('og:image', $post->cover->url());
MetaTag::set('og:url', $post->canonicalUrl());
MetaTag::set('twitter:title', $post->title);

Canonical URLs and paginated pages. Set canonical on every indexable page and vary it for pagination (?page=2) where appropriate. MetaTag::set('canonical', $url) is enough if the package renders it as <link rel="canonical">; if not, render it manually alongside {!! MetaTag::render() !!}.

JSON-LD alongside the package. The package handles meta/link tags, not structured data. Add a Blade stack for @push('schema') and emit your own <script type="application/ld+json">. Don't try to force Article/BreadcrumbList through MetaTag.

Inertia/Livewire caution. For a fully client-rendered SPA, these server-side tags will only ever describe the initial shell. Either accept that (and let the frontend manage <head>), or add SSR. For Livewire, re-set tags in updated()/mount() because navigating between components doesn't rerun the controller.

Gotchas and Tips

  • Use {!! MetaTag::render() !!}, never {{ }}. render() returns HTML; escaping it will print <meta ...> as visible text in the page. This is the single most common "it's broken" report.
  • Escape your values yourself. description, title, and image typically flow straight into attribute values. Run them through e() or Blade's {{ }} semantics before handing to set(), otherwise a post title containing " breaks the tag.
  • Absolute URLs for og:image. asset('...') is fine, but a relative path or a Storage::url() without a domain will fail most crawlers and social debuggers. Use url()/asset() or a configured CDN base.
  • Truncate descriptions. Google truncates around 155–160 characters and Facebook around 60–100 for titles. Trim in your model/presenter, not in the view, so it's testable.
  • Clear config cache after publishing. php artisan config:cache in deploy scripts is expected; if tags vanish or defaults look stale, php artisan config:clear && php artisan cache:clear before assuming a bug.
  • Debug by inspecting source, not the facade. dump(MetaTag::get('title')) tells you what was set, but the rendered order is what crawlers see. Always verify with "View Page Source" and a couple of social debuggers (Facebook Sharing Debugger, X Card Validator).
  • Watch for double-set collisions. If both a middleware and a controller set title, whichever runs last wins — silently. Name your tag keys consistently (title vs og:title vs meta_title) or you'll get duplicate/conflicting tags.
  • Response caching will freeze your tags. If you cache full HTML responses, per-user or per-page titles get baked in. Either cache only the fragment below <head>, or include the meta data in the cache key.
  • remove() is your friend for opt-outs. For "noindex" admin or filtered listing pages, explicitly MetaTag::remove('canonical') and set robots to noindex,nofollow rather than leaving defaults in place.
  • Extension points. The package is designed to be extended: publish the config, override the Blade view for the head, and add custom tags via MetaTag::set() with arbitrary names (the renderer emits them as <meta name="...">). If you need <link> tags beyond canonical, either extend the package's view or render those manually — don't fight the tool.
  • Keep it out of AppServiceProvider::boot() unconditionally. Setting tags globally there runs even for API routes and queues; scope to web routes/middleware so jobs don't accumulate meta state.
  • Test it. A single feature test asserting assertSee('<meta property="og:title"', false) on your post page catches the escaping and ordering mistakes that are otherwise only visible in production.
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