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

Seotools Laravel Package

apility/seotools

Apility SEO Tools is a Laravel package for managing SEO metadata from PHP and Blade. Configure meta tags, Open Graph and Twitter cards, canonical URLs, robots directives, and structured data through a fluent API for cleaner, more maintainable templates.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require apility/seotools

Laravel's package auto-discovery registers Apility\SEOTools\SEOToolsServiceProvider and the SEOTools facade alias. On Lumen or with discovery disabled, register the provider manually and add 'SEOTools' => Apility\SEOTools\Facades\SEOTools::class to your aliases.

Publish the config first — this is the file you'll actually live in

php artisan vendor:publish --provider="Apility\SEOTools\SEOToolsServiceProvider"

This drops config/seotools.php with meta, opengraph, twitter and json-ld sections. Every section has a defaults array; defaults ship as false, so nothing renders until you either fill them in or set values at runtime. Spending ten minutes on this file up front saves you from repeating the same setSite() / default-image calls in every controller.

Render once, in your layout

{{-- resources/views/layouts/app.blade.php --}}
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    {!! SEOTools::generate() !!}
</head>

generate() returns a concatenated HTML string — it must be output unescaped ({!! !!}), and it should appear exactly once per response.

First use case: per-page tags from a controller

use Apility\SEOTools\Facades\SEOTools;

public function show(Post $post)
{
    SEOTools::setTitle($post->title);
    SEOTools::setDescription(Str::limit(strip_tags($post->excerpt), 155));
    SEOTools::setCanonical(route('posts.show', $post));

    SEOTools::opengraph()
        ->setType('article')
        ->setTitle($post->title)
        ->setUrl(route('posts.show', $post))
        ->addProperty('article:published_time', $post->published_at->toIso8601String());

    SEOTools::twitter()
        ->setType('summary_large_image')
        ->setTitle($post->title);

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

Implementation Patterns

Config for the 90% case, facades for the exceptions. Put site name, twitter handle, default OG image, locale and separator in config/seotools.php. Override per route in the controller. Anything more clever than that belongs in a middleware or view composer, not scattered through Blade.

Global defaults via middleware. The most common real-world pattern is a SeoDefaults middleware that sets site-level tags before the controller runs, so controllers only touch page-specific values:

public function handle($request, Closure $next)
{
    SEOTools::setTitle(config('app.name'), false);
    SEOTools::opengraph()->setSiteName(config('app.name'));
    SEOTools::twitter()->setSite('@myhandle');

    return $next($request);
}

Model-driven SEO. A small HasSeo trait that maps model columns (or a seo_meta JSON column, or a polymorphic seo_metas table) onto the facades keeps controllers thin and makes the tags testable. This is the pattern that scales when marketing asks for editable titles per record.

Blade component wrapper. If designers need to inject tags mid-page (e.g. article body), expose a <x-seo :model="$post" /> component that calls the trait, rather than letting Blade templates reach into the facade directly.

JSON-LD as a first-class concern. Use SEOTools::jsonLd()->setType('Article')->addValue('headline', $post->title) and dot-notation for nesting (addValue('author.name', $post->author->name)). When a page needs more than one graph node (Article + BreadcrumbList + Organization), use the multi-JSON-LD helper rather than merging arrays by hand.

Testing. Because the facade mutates a shared container, assert against SEOTools::generate() inside a feature test that hits a real route — that catches both "forgot to set the title" and "rendered the wrong thing twice". Reset the singleton in setUp() (or a base TestCase) so state doesn't bleed between tests.

Sitemap and robots. These are opt-in, config-driven features. Register their routes only in the environments where they matter (usually not local), and drive them from the config file rather than hardcoding paths.

Gotchas and Tips

  • Empty defaults are the #1 confusion. 'title' => false in config/seotools.php means <title> is simply absent. If tags are missing, check the config before you check your code.
  • addMeta() argument order is (name, content, attribute) — the third parameter is the attribute name ('property' for OG, 'name' by default). Getting this backwards produces tags that look fine but are invisible to crawlers.
  • Open Graph needs an explicit type. setType('article') before adding article:* properties; otherwise the og:type tag is missing or wrong and article metadata gets ignored by validators.
  • The container is a singleton. Under Octane, in queued jobs that render views, and across long-running test suites, state accumulates. Reset in middleware terminate() or at the top of each request/job.
  • generate() output is raw HTML. Never interpolate user input into setTitle() / setDescription() without sanitising — you are writing directly into <head>.
  • Title separator and append behaviour live in config (separator) and in the second argument of setTitle($title, $appendDefault). Decide once whether page titles get the site name appended; retrofitting it later means touching every controller.
  • Canonical URLs are not automatic. Set them explicitly (paginated pages especially), or you'll ship duplicate-content canonical gaps.
  • Byte-for-byte output is not a stable contract. Tag order and attribute formatting can shift between minor versions. If you assert on rendered markup in tests, assert on substrings (assertSee('og:title', false)), not full string equality — or you'll be rewriting tests on every bump.
  • Cache generated markup, not the facade state. Cache::remember("seo:{$route}:{$locale}", ...) around the values you feed in is safe; caching the SEOTools instance itself is not.
  • Extension points: the sub-services (metatags(), opengraph(), twitter(), jsonLd()) are resolved from the container, so you can bind your own implementation or decorate them if you need custom tag sets. Published views can be overridden if you need to reorder output.
  • On this release: v2.0.2 ships with only a changelog link and no notes. Treat it as an untriaged patch — pin ^2.0 (or even 2.0.1) and skim the diff before upgrading anything that renders customer-facing markup you can't easily re-verify.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console