michael-rubel/laravel-seo-manager
Laravel SEO Manager gives your app a clean, configurable way to handle SEO metadata—page titles, meta descriptions, canonical URLs, Open Graph and Twitter tags, robots directives, and sitemaps—so every page is search-ready without cluttering your controllers or Blade views.
The package is archived (last release May 2022, 20 stars), so treat it as a small, readable dependency rather than a long-term platform choice. Budget ~15 minutes to read the source before wiring it in.
composer require michael-rubel/laravel-seo-manager
php artisan vendor:publish --tag=seo-manager-config
The service provider is auto-discovered. If your app has discovery disabled, register MichaelRubel\SeoManager\SeoManagerServiceProvider manually in config/app.php.
Where to look first, in order:
config/seo-manager.php — this is the whole feature. It maps URL patterns to a keyed array of tags (title, description, keywords, etc.) and defines fallback defaults.src/SeoManagerServiceProvider.php — confirms the container binding, facade alias, helper function, and whether a middleware is registered for URL resolution.src/SeoManager.php (or similarly named class) — the URL matcher and the getter/setter surface you will call from Blade and controllers.seo(...) or SeoManager::...) is what you will use 95% of the time.First use case — give the homepage real meta tags:
// config/seo-manager.php
'urls' => [
'/' => [
'title' => 'Acme — Industrial Fasteners',
'description' => 'Same-day shipping on metric and imperial fasteners.',
'canonical' => 'https://acme.test/',
],
'about' => [
'title' => 'About Acme',
'description' => 'Family-owned since 1974.',
],
],
Then in your layout head:
<title>{{ seo('title') }}</title>
<meta name="description" content="{{ seo('description') }}">
<link rel="canonical" href="{{ seo('canonical') }}">
Confirm the exact accessor name in the service provider before copying — the package is small enough that reading src/ is faster than guessing.
resources/views/partials/seo.blade.php and @include it in the single layout. This makes it trivial to add Open Graph/Twitter tags the package does not cover.defaults block (site name, default description, fallback image) and treat anything in urls as an override. Merge logic in your own view composer if the package does not do it, so a missing key never renders an empty attribute.blog/how-to-ship before blog/*, and put the catch-all last. If ordering is config-array order, keep the file grouped by section (static pages, blog, product).posts/{slug} or products/{sku} the right title lives in the database. Use the manager's runtime set(...)/setTitle(...) API in a controller, middleware, or View::composer('*', ...) after route model binding resolves the model. Do not try to enumerate thousands of URLs in config./en/... and /de/..., either add both variants to urls or normalize the path in a lightweight middleware before resolution. A single regex that strips ^/(en|de) is usually enough.og:type, and product JSON-LD are easier to compute from the bound model than to store in config. Compose them alongside the manager's output in partials/seo.blade.php./, /about, and one dynamic route and asserts assertSee('<title>', false) with the expected string. The package ships no test helpers, so this is your regression net.php artisan config:cache in production; the config file is the entire data source, so this matters more than usual for a meta-tag package.composer.json for illuminate/* and PHP constraints before installing on Laravel 10/11. On a newer framework you will likely need --ignore-platform-req or a fork. There are no security or compatibility patches coming.src/ — it is a few hundred lines — and prefer extending/overriding over searching.www vs non-www, subdomains, query strings (?page=2), and locale segments all cause silent misses that fall back to defaults. Log the resolved key for a day during rollout.config:cache vs runtime mutation. Never call config(['seo-manager.urls...' => ...]) at runtime in production — the config is already cached and the manager may have resolved earlier. Use the manager's setters or a view composer.{{ }} for values (title, description, canonical). Only use {!! !!} if the manager returns pre-rendered <meta> strings; otherwise you are opening an XSS hole from DB-driven titles.@if(seo('description'))<meta name="description" content="{{ seo('description') }}">@endif, or normalize defaults in a composer.sitemap.xml, and robots.txt are your responsibility. Plan to layer spatie/laravel-sitemap or a hand-rolled route for those.$this->app->singleton(SeoManager::class, MySeoManager::class) is cheap, and vendoring the ~few files into app/Support/Seo is a legitimate exit strategy if the archived status becomes a problem.request()->path() replicates the core behavior. Evaluate that trade-off early rather than after you have hundreds of URL entries in config.How can I help you explore Laravel packages today?