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

Laravel Seo Laravel Package

elegantly/laravel-seo

Elegantly Laravel SEO provides a fluent way to manage meta tags, Open Graph, Twitter cards, canonical URLs, robots directives, and JSON-LD structured data in Laravel apps. Configure SEO per model or page and render it in Blade with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require elegantly/laravel-seo
php artisan vendor:publish --provider="Elegantly\Seo\SeoServiceProvider"

The provider should be auto-discovered. If it isn't (older Laravel or a manual setup), register Elegantly\Seo\SeoServiceProvider in bootstrap/providers.php (Laravel 11+) or config/app.php. Confirm the real namespace against the autoload block in the package's composer.json before you write any imports — with 12 stars there's no ecosystem of tutorials to correct you.

Where to look first, in this order

  1. vendor/elegantly/laravel-seo/README.md — the only authoritative usage docs.
  2. vendor/elegantly/laravel-seo/src/SeoServiceProvider.php — tells you what is registered: a singleton, a Blade directive, a component, middleware, or facade alias. This one file dictates your whole integration strategy.
  3. vendor/elegantly/laravel-seo/src/ tag classes — how a tag is modelled (name, attributes, content) determines whether you can extend it cleanly.
  4. config/seo.php after publishing — defaults, site name, separators, social handles.

First use case (do this before designing anything)

Render a title and description on one page and view the raw HTML:

{{-- resources/views/layouts/app.blade.php --}}
<head>
    {!! seo()->for($page ?? null) !!}
</head>

Or, if the package exposes a component instead of a helper:

<x-seo :model="$page" />

Then curl -s https://your.test/about | head -40. Do not use browser devtools for this — you want the server-rendered <head>, not the post-JS DOM. Once you see a <title> and <meta name="description"> in the response body, you understand the contract and can build on it.

Implementation Patterns

Defaults in config, overrides per page. Keep site name, title separator, default OG image, and Twitter handle in config/seo.php. Set only the page-specific title/description in the controller or view. This is the single highest-leverage pattern: it means a new page is one line of code, and a rebrand is one config change.

// config/seo.php (shape will vary — match what the package ships)
return [
    'site_name' => config('app.name'),
    'separator' => ' | ',
    'defaults'  => [
        'image' => '/images/og-default.png',
        'twitter' => '@yourhandle',
    ],
];

Controller-level assembly for dynamic pages. Build tags where you already have the model and its relationships, then pass the finished object to the view. Avoid calling SEO helpers from inside Blade loops or partials — that's how you get duplicate tags and N+1 queries on og:image relationships.

public function show(Post $post)
{
    seo()
        ->title($post->meta_title ?? $post->title)
        ->description(Str::limit(strip_tags($post->excerpt), 155))
        ->image($post->ogImageUrl())
        ->canonical(route('posts.show', $post))
        ->type('article');

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

A model trait for content-driven SEO. If editors control titles and descriptions in the CMS, put the mapping in one place so it can't drift between controllers:

trait HasSeo
{
    public function seoTitle(): string
    {
        return $this->meta_title ?: $this->title;
    }

    public function seoDescription(): string
    {
        return $this->meta_description
            ?: Str::limit(strip_tags($this->body), 155);
    }
}

Then each controller is seo()->from($post) or a short chain of the two accessors — and your fallback chain (meta field → content excerpt → site default) is testable in isolation.

Render once, in the layout. A single render point in <head> is what makes the "defaults + overrides" model work. If you also hand-write <meta property="og:title"> in a partial, you'll ship duplicates and social scrapers will pick the wrong one.

Structured data as a separate concern. JSON-LD (Article, Product, BreadcrumbList) is schema.org, not meta tags. If the package supports it, keep those definitions in dedicated builder classes or Blade components per type rather than inlining arrays in controllers — they're long, they change with Google's guidance, and they deserve their own tests.

Testing. Assert on the rendered head rather than on internal objects, so the tests survive a package swap:

$this->get('/posts/hello')
    ->assertOk()
    ->assertSee('<title>Hello World | Acme</title>', false)
    ->assertSee('name="description"', false)
    ->assertSee('rel="canonical"', false);

Write one "SEO smoke test" per resource plus a unit test for the fallback chain. That's enough coverage for something this small and it catches the regressions that actually matter (empty description, missing canonical, duplicated OG image).

Keep sitemaps and redirects out of it. This package is a tag builder. spatie/laravel-sitemap for sitemap.xml, and your own middleware or spatie/laravel-redirects for 301s. Don't try to make one package own all of SEO.

Gotchas and Tips

Escape everything that comes from the CMS. SEO tags are usually rendered unescaped ({!! !!}) because they contain markup. That makes a user-editable meta_title an XSS vector. Escape at the boundary — e() / {{ }} on the value — before it reaches the builder, and never pass raw editor HTML into a description tag without strip_tags.

Duplicate tags are the #1 failure mode. Two <title> tags, two og:image tags. Cause is almost always rendering in both the layout and a child view, or a package default plus a manual tag. When debugging, count occurrences in curl output: curl -s $URL | grep -c '<title>'. If the package dedupes by key, verify it actually does before relying on it.

request()->url() vs app.url for canonicals. Behind a load balancer, canonical URLs will silently become http:// or an internal hostname. Configure TrustProxies and URL::forceScheme('https') in AppServiceProvider, then assert the canonical in a test. This is the bug that costs you indexing and takes three weeks to notice.

Canonical trailing slashes must be consistent with what your routes actually serve, or you'll publish canonicals pointing at a redirect. Generate them with route() / url(), never hand-concatenate.

SPAs and Inertia need a server-side answer. If you render with Inertia or Livewire, meta tags injected client-side are unreliable for crawlers and social scrapers. Either render the head server-side via a root Blade template with a shared prop, or use SSR. Test with curl, not the browser — a passing browser check here proves nothing.

Response caching will freeze your first page's tags. If you cache full responses, make sure the cache key includes the locale and any SEO-relevant variation, or every page will serve the homepage's title.

Character limits are a content problem, not a code problem. Google truncates titles around 60 characters and descriptions around 155. Add a truncation helper in HasSeo and, better, surface a warning in your admin UI. The package will happily render a 400-character description.

Debugging: dump the tag collection, not the HTML. Find the accessor that returns the built tags (a toArray(), all(), or Collection property) and dd() it in a tinker session or a throwaway route. Reading a rendered head string to find why one attribute is missing is slow; reading the array is instant.

Verify it's actually in composer.json as a direct dependency and pin it. 12 stars and zero dependents means no community to catch a bad release. Use "elegantly/laravel-seo": "^1.0" (match the real major) and treat upgrades as reviewable events, not routine composer update churn.

Wrap it behind a thin adapter if SEO is load-bearing. Put your own SeoBuilder / helper in app/Support/ that delegates to the package. Twenty lines of indirection means that if the package goes unmaintained — a real risk at this size — swapping to a different builder or hand-rolled tags is an afternoon, not a refactor of every controller.

Extension points worth checking in src/: whether you can register a custom tag class (for tags the package doesn't model, e.g. og:video or non-standard verification tags), whether the renderer respects insertion order, and whether there's a macro on the builder. If none exist, a raw-HTML escape hatch (->raw('<meta ...>') or appending a HtmlString) is the minimum you need — confirm it exists before committing to the package.

Don't let it own your critical path. Title, description, and canonical are the whole value proposition. Everything else — hreflang, JSON-LD, sitemaps — should have a documented manual fallback in your codebase.

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