vinicius73/seotools
SEOTools is a Laravel package that simplifies search engine optimization for your applications. It provides helpers to generate meta tags, Open Graph and Twitter Card data, manage sitemaps and robots.txt, and integrate analytics, keeping SEO configuration clean and centralized.
This is a Laravel 5-era SEO helper (last release 2016), so treat it as a "meta tag assembler in a facade" rather than a modern SEO suite. First thing to do is confirm it still boots on your Laravel version — if you're on 5.5+ it may work with the auto-discovery shim, but on Laravel 6+ you'll almost certainly need to fix the service provider / container bindings before anything renders.
Install and pin. composer require vinicius73/seotools. Since the package is abandoned-by-inactivity, pin the exact version in composer.json and don't trust ^ constraints — a composer update months later can surface an unresolved dependency.
Register the provider. In Laravel ≤5.4, add Vinicius73\SeoTools\Providers\SeoToolsServiceProvider::class to config/app.php under providers, and register the facade alias (usually 'Seo' => Vinicius73\SeoTools\Facades\Seo::class). On 5.5+ check whether the provider is actually listed in composer.json's extra.laravel.providers — if not, register manually.
Publish config. php artisan vendor:publish --provider="Vinicius73\SeoTools\Providers\SeoToolsServiceProvider". This usually drops a config/seotools.php (or seo.php) with defaults for title separator, site name, default description, and possibly Open Graph defaults. Read this file first — it is the single best map of what the package actually does. If the publish tag is wrong, use --tag=config.
Look at the source before the README. With 23 stars and a 2016 release, the README is likely thin and partly out of date. vendor/vinicius73/seotools/src/ is small — skim the facade and the main service class to see what methods actually exist (setTitle, setDescription, addMeta, addOpenGraph, render, etc.). Trust the source over the docs.
First use case. In a controller action, set metadata, then output it in your layout:
// Controller
public function show(Post $post)
{
Seo::setTitle($post->title);
Seo::setDescription(str_limit(strip_tags($post->body), 155));
return view('posts.show', compact('post'));
}
{{-- layouts/app.blade.php --}}
<head>
{!! Seo::render() !!}
</head>
That single flow — set in controller, render in layout — is essentially the whole mental model of the package.
Default-then-override in a base controller or middleware. Don't set SEO on every action. Put sensible site-wide defaults in config/seotools.php (site name, separator, default description) and only call the facade in actions that need to override. If the package doesn't merge defaults cleanly, wrap it in your own SeoService so controllers depend on your abstraction, not the vendor API.
// app/Http/Middleware/SeoDefaults.php
public function handle($request, Closure $next)
{
Seo::setTitle(config('app.name'));
Seo::setDescription(config('seotools.default_description'));
return $next($request);
}
Per-model SEO via a trait or accessor contract. For blogs/products, the pattern that scales best is a small interface (Seoable) with seoTitle() / seoDescription() methods, and a helper that hydrates the facade from a model. This keeps metadata next to the content that owns it instead of scattered across controllers.
trait HasSeo
{
public function applySeo()
{
Seo::setTitle($this->seo_title ?: $this->title);
Seo::setDescription($this->seo_description ?: str_limit(strip_tags($this->body), 155));
Seo::addMeta('og:type', 'article');
if ($this->image) {
Seo::addOpenGraph('image') ?? Seo::addMeta('og:image', url($this->image));
}
return $this;
}
}
Note the ?? — many 2016-era SEO packages have an inconsistent API where some tags go through a dedicated method (addOpenGraph) and others only through the generic addMeta. Probe both in tinker before committing to one.
Open Graph / Twitter cards are the highest-value use case. Plain <title> and <meta name="description"> have been deprecated by Google for years; the durable value here is generating og:* and twitter:* tags for link previews on WhatsApp, Slack, Facebook, and Twitter — which matters a lot for a Brazilian audience where WhatsApp sharing is primary. Make sure og:image is an absolute URL and at least 1200×630; the package can't enforce this, so validate it yourself.
Render ordering matters. The facade is a request-scoped singleton, so anything that sets SEO must run before the view renders — controller actions and middleware are fine, Blade components are not (they render after <head> is already flushed). If you use view composers, register them so they run before the layout, not inside a partial.
Layout partial for the <head> block. Keep a resources/views/partials/seo.blade.php containing only {!! Seo::render() !!} and @include it. That gives you one place to swap in a modern replacement later without touching 40 layouts.
Sitemap / robots, if the package provides them. Some seotools packages ship sitemap and robots generators. If this one does, wire the sitemap to a route and a scheduled command ($schedule->command('seo:sitemap')) rather than generating on request — on a content-heavy site request-time generation will become a performance problem.
config merging API changes or typed method signatures; on 8+ with PHP 8 you may hit deprecation notices. Budget an hour to read the source and patch, or more realistically, fork it into app/Support/Seo.Seo (arcanedev/seotools, spatie, artesaos/seotools are common), you'll silently get the wrong class. Use the fully-qualified class name in use statements rather than relying on the alias.{!! Seo::render() !!}. Using {{ }} double-escapes and produces literal <meta...> in the head. Conversely, audit what you feed into the setters — user-supplied titles with quotes can break out of content="..." attributes. Sanitize with e() or strip_tags() before passing.<meta name="description" content="">), which is worse than no tag. Either don't call setters when the value is falsy, or wrap the render in a check.php artisan config:cache, published config changes are invisible until you re-cache. If defaults "stop working" in staging, this is usually why.dd(Seo::render()) in a route closure is faster than re-reading the source. It instantly tells you which tags the package supports and how it formats them.SeoPresenter class that returns an array of meta tags, with the Blade partial iterating over it. You get full control of markup, keep the package doing only its data-collection job, and can drop it later by changing one composer binding.spatie/laravel-sitemap + hand-rolled meta + JSON-LD in the layout.How can I help you explore Laravel packages today?