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

ralphjsmit/laravel-seo

Laravel SEO package that generates valid meta tags out of the box: titles, descriptions, OpenGraph, Twitter, structured data, favicons and robots. Attach SEO data to models or pull it dynamically from your model with a simple API.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish (5 minutes):

composer require ralphjsmit/laravel-seo
php artisan vendor:publish --tag="seo-migrations"
php artisan vendor:publish --tag="seo-config"
php artisan migrate

Where to look first — in this order:

  1. config/seo.php — the only file you must touch on day one. Everything else has sane defaults.
  2. RalphJSmit\Laravel\SEO\Support\SEOData — the DTO that is the real API surface of the package. Read its constructor signature; that's your whole vocabulary.
  3. RalphJSmit\Laravel\SEO\Support\HasSEO — the trait you'll put on models.
  4. The package's Blade views (publishable) — you'll want to know what markup it emits before you go debugging "why is there no og:image".

Minimum viable config — set these before writing any code, because they affect every page:

'site_name'   => 'My Project',
'title'       => ['suffix' => ' | My Project', 'homepage_title' => 'My Project — ...'],
'description' => ['fallback' => 'One-sentence pitch of the whole site.'],
'image'       => ['fallback' => 'images/og-default.jpg'],
'twitter'     => ['@username' => 'myhandle'],

Wire the layout once. In resources/views/layouts/app.blade.php, put the render call in <head> and delete your hand-written <title>:

<head>
    <meta charset="utf-8">
    {!! seo() !!}
</head>

First use case to ship: static marketing pages. Pass a SEOData straight from the controller — no model, no database row:

return view('pages.home', [
    'SEOData' => new SEOData(
        title: 'Awesome News — Independent Tech Journalism',
        description: 'Daily reporting on ...',
        image: 'images/og/home.jpg',
    ),
]);
{!! seo($SEOData ?? null) !!}

That alone gives you title, description, canonical, robots, OpenGraph, Twitter cards and an inferred-URL title fallback for every page you forgot. Then move to models.

Implementation Patterns

The model pattern — derive, don't duplicate

The single biggest productivity win is getDynamicSEOData(): SEO values are computed from columns you already maintain, so editors never touch a second form.

class Post extends Model
{
    use HasSEO;

    public function getDynamicSEOData(): SEOData
    {
        return new SEOData(
            title: $this->title,
            description: $this->excerpt,
            author: $this->author->full_name,
            image: "images/posts/{$this->slug}.jpg",
            published_time: $this->published_at,
            modified_time: $this->updated_at,
            section: $this->category->name,
            tags: $this->tags->pluck('name')->all(),
        );
    }
}

Then in the layout: {!! seo($post ?? null) !!}. Use the trait's seo relation only for the fields a human should override (usually title, description, image, robots), and let the dynamic method handle the rest.

Precedence is your architecture

Memorise the merge order (lowest → highest):

  1. config/seo.php defaults
  2. $post->seo (the database row)
  3. getDynamicSEOData()
  4. SEOManager::SEODataTransformer($closure)

Practical consequence: put anything the marketing team must be able to edit into step 2, and put anything computed into step 3. If you find yourself writing a CmsSeo table per model type, you're fighting the package.

The transformer — one place for app-wide rules

Register once in AppServiceProvider::boot():

SEOManager::SEODataTransformer(function (SEOData $SEOData, $model) {
    // Never index staging / preview URLs
    if (! app()->isProduction()) {
        $SEOData->markAsNoIndex();
    }

    // Guarantee every page has a shareable image
    $SEOData->image ??= 'images/og-default.jpg';

    // Trim runaway descriptions before they hit the meta tag
    $SEOData->description = Str::limit($SEOData->description, 155);

    return $SEOData;
});

This is the correct extension point for: environment noindex, character clamping, per-tenant site names, and last-minute A/B of titles. It's also the easiest thing to unit test.

Non-model pages: search, filters, checkout, thank-you

Don't create a model for these. Build SEOData in the controller or a small SeoPresenter, and be deliberate about robots:

new SEOData(
    title: 'Search results',
    robots: 'noindex,follow',           // or $SEOData->markAsNoIndex()
    url: url()->current(),              // override to include ?page=N if needed
)

Anything that produces infinite URL permutations (?sort=, ?filter=, ?page=) should be noindex,follow; keep the crawl budget for real content.

Structured data

Attach JSON-LD only where the visible page actually matches it. Article + BreadcrumbList on blog posts is the highest-value pairing; FAQPage on support pages is next. Add a custom schema class for domain types (Recipe, Product, Event) by extending the package's schema classes — don't hand-write <script type="application/ld+json"> in Blade, you lose the collection's ability to nest.

Validate with Google's Rich Results Test per template, not per page.

Multi-locale

Pass locale from app()->getLocale() (the default) and supply alternates as AlternateTag entries for every translated URL including x-default. If your translated URLs are /{locale}/..., generate the alternates from the same route helper you use for the switcher so they can't drift.

Inertia

Put {!! seo() !!} in the root Blade template and share the SEO payload as a page prop ('SEOData' => new SEOData(...)), then pass it through. The package tags the title with inertia so titles update on client-side navigation, but that's the only tag that changes after the first load — see gotchas.

Testing

Assert on rendered HTML, not on the manager:

$this->get("/posts/{$post->slug}")
    ->assertSee('<title>', false)
    ->assertSee('og:title', false)
    ->assertSee('noindex', false);

Add one test per layout variant (post, static page, search page) and you'll catch 90% of SEO regressions.

Gotchas and Tips

Duplicate <title> is the #1 bug. The package renders <title>; leaving your own in the layout gives you two, and Google takes the first. Grep your layouts and any <x-head> component.

Images must be public-folder relative. image is passed through secure_url() after being resolved from public_path(). Filenames in storage/app/public work only once you've run storage:link and pass 'storage/images/...'. Absolute https://cdn... URLs are the wrong tool here — override the rendered view or store a relative path and let asset() resolve.

secure_url() inherits the request host and scheme. Behind a proxy or in a queue/CLI context, that can emit http:// or an internal hostname. If you render SEO tags anywhere except a normal web request (emails, PDFs, sitemap generation), set TrustProxies correctly or precompute absolute URLs.

infer_title_from_url will happily title a page "12345". Any route with an ID, UUID or hash in the path produces a garbage title. Either disable inference and require explicit titles, or override it per route group.

Canonical tags ignore query strings. url()->current() strips ?page=2, so every paginated page canonicalises to page 1. Google recommends self-referencing canonicals on paginated series — override url in SEOData when request()->has('page').

Existing records have no seo row. HasSEO hooks model creation; rows that predate the package won't get one. Write a one-off backfill ($model->addSEO() in a chunked loop) or your ->seo accessor will be null and templates that call $post->seo->update() will fatal.

Lazy loads become N+1s. Rendering {!! seo($post) !!} in a loop touches seo, author, category, tags. Eager load all of them in the controller query; a listing page can otherwise triple its query count.

Inertia only refreshes the title. After client-side navigation, meta tags remain those of the first server-rendered page unless you also manage head state client-side. For SPAs with heavy navigation, plan for a head component plus the server-rendered tags on first load.

robots.force_default is a blunt instrument — and it's the right one for staging. Set it to noindex,nofollow in the staging .env so nobody has to remember per-page rules. Put a test or a deploy check that asserts it's false in production.

The package does not generate sitemap.xml or robots.txt. Pair it with spatie/laravel-sitemap for the sitemap, then point config('seo.sitemap') at /sitemap.xml (leading slash — it's resolved relative to the root domain). Reference the same path in your robots.txt by hand.

Nothing truncates your descriptions or titles. Long excerpt columns flow straight into <meta name="description"> and get cut mid-word by search engines. Clamp in the transformer (see above) rather than in every model.

Homepage titles skip the suffix. If homepage_title is set, the suffix is not appended — by design, but it surprises people who set 'suffix' => ' | Brand' and get an inconsistent homepage.

Customising markup: publish the package views and edit them, rather than string-replacing the rendered output. Keep your edits minimal and re-check them on package upgrades — the render call returns one big HTML string, so post-processing it with str_replace is fragile.

Debugging checklist when a tag is missing: (1) is seo() called with the model, (2) does the model have the trait, (3) is the value null and therefore falling back to config, (4) did the transformer wipe it, (5) is the tag conditional on something (e.g. og:image needs an image and site_name needs config). Walking that in order beats reading the source.

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.
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
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource