Table of Contents

Introduction

Laravel is a magnificent framework for building web applications, but its dynamic nature introduces unique SEO challenges. Unlike a static HTML site where every URL maps to a fixed file, a Laravel application generates pages on the fly — rendering Blade views, querying databases, and resolving routes at runtime. Search engines need clear signals to understand and rank this content.

The goal of this article is simple: give you a battle-tested toolkit for Laravel SEO. We’ll look at what Laravel gives you out of the box, survey the best community packages (with real code examples), and show you how to let non-technical team members manage meta tags without touching a line of code.


1. Laravel’s Native SEO Helpers

Before reaching for a package, it’s worth knowing what Laravel already does well.

Clean URLs with Route Naming

Laravel’s routing system lets you define semantic URLs that search engines love:

// Instead of /posts?category=seo&id=42
Route::resource('posts', PostController::class);
// Generates: /posts, /posts/{post}, /posts/{post}/edit

Use Route::name() and named routes to generate consistent URLs throughout your app — this avoids duplicate content from multiple URL patterns pointing to the same page.

Dynamic Title Tags with Blade Sections

Blade’s section inheritance is a simple yet effective SEO tool:

<!-- layouts/app.blade.php -->
<title>@yield('title', config('app.name'))</title>

<!-- posts/show.blade.php -->
@extends('layouts.app')
@section('title', $post->title . ' — My Blog')

Caching for Speed

Page speed is a confirmed ranking factor. Laravel’s caching layer is your first line of defense:

// Cache an entire response for anonymous visitors
use Illuminate\Support\Facades\Cache;

$posts = Cache::remember('posts.index', 3600, fn () =>
    Post::with(['author', 'tags'])->published()->get()
);

// Or use spatie/laravel-responsecache for full-page caching

robots.txt and Sitemaps

Serve a dynamic robots.txt directly from a route:

Route::get('robots.txt', function () {
    $content = "User-agent: *\nAllow: /\nSitemap: " . url('sitemap.xml');
    return response($content)->header('Content-Type', 'text/plain');
});

For sitemaps, the spatie/laravel-sitemap package is the de facto standard — it generates XML sitemaps from your models and routes with a single Artisan command.


2. Top SEO Packages for Laravel (2025 Update)

The Laravel community has produced several excellent SEO packages. Here is how they compare:

PackageStarsMaintenanceKey FeatureInstall Difficulty
ralphjsmit/laravel-seo~850Active (v1.8.1, Mar 2026)Model-based SEO with HasSEO traitEasy
artesaos/seotools~3.3kMaintained (v1.4.0, Mar 2026)Multiple facades (SEOMeta, OpenGraph, TwitterCard, JsonLd)Easy
romanzipp/laravel-seo~352Maintained (v2.6.0, Oct 2023)Laravel Mix asset preloading, maximum customizationMedium
elegantly/laravel-seo~12Active (v1.4.0, Apr 2026)Flexible tag management, fluent APIEasy
butschster/LaravelMetaTags~630ActiveFramework-agnostic meta tags, Inertia supportMedium

This package, featured on Laravel News in August 2024, takes a unique approach: it attaches SEO data directly to your Eloquent models. If you are familiar with Spatie’s Media Library, the pattern will feel right at home.

Installation:

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

Basic usage — render sensible defaults immediately:

<head>
    {!! seo() !!}
</head>

That single line outputs a <title> tag, meta description, Open Graph tags, Twitter Cards, canonical URL, and robots meta — all with sensible defaults from config/seo.php.

Model-based SEO — the real power:

use RalphJSmit\Laravel\SEO\Support\HasSEO;

class Post extends Model
{
    use HasSEO;
}

This creates a one-to-one seo relationship automatically. You can update SEO data like this:

$post = Post::find(1);
$post->seo->update([
    'title' => 'My Great Post',
    'description' => 'A compelling description for search results.',
    'image' => 'images/posts/1.jpg',
]);

But the killer feature is getDynamicSEOData(), which lets you pull SEO values directly from your model’s existing fields without any duplication:

use RalphJSmit\Laravel\SEO\Support\SEOData;

class Post extends Model
{
    use HasSEO;

    public function getDynamicSEOData(): SEOData
    {
        return new SEOData(
            title: $this->title,
            description: str($this->body)->limit(160),
            image: $this->featured_image,
            author: $this->author->name,
            published_time: $this->created_at,
        );
    }
}

Now {!! seo()->for($post) !!} in your Blade view automatically pulls live data from the model. The resolution order is: getDynamicSEOData() > associated SEO model > config defaults — so the most specific value wins.

The package also supports structured data via SchemaCollection:

use RalphJSmit\Laravel\SEO\SchemaCollection;

public function getDynamicSEOData(): SEOData
{
    return new SEOData(
        title: $this->title,
        schema: SchemaCollection::make()
            ->addArticle()
            ->addBreadcrumbs(function ($breadcrumbs) {
                return $breadcrumbs->prependBreadcrumbs([
                    'Home' => url('/'),
                    'Blog' => url('/blog'),
                ]);
            }),
    );
}

Artesaos/seotools (The Veteran)

With over 3,300 stars, artesaos/seotools is the oldest and most widely known Laravel SEO package. It was the go-to solution for years and is still maintained (latest v1.4.0 supports Laravel 13). However, it shows its age in a few ways:

Where it still shines is its comprehensive Open Graph support — it handles every OG namespace (article, book, profile, music, video, place) with dedicated methods like setArticle(), setProfile(), etc. If you need fine-grained OG control, it is unmatched.

Why newer packages are replacing it: the “set everything manually in the controller” approach creates duplication and makes it hard for non-developers to manage SEO. Modern packages like ralphjsmit/laravel-seo automate this by binding SEO data to models.

Romanzipp/laravel-seo (Customization + Mix)

Romanzipp/laravel-seo takes a different approach — it is a low-level, maximum-flexibility SEO toolkit. Instead of model binding, you build your SEO tag collection programmatically.

Its standout feature is Laravel Mix integration: it can automatically preload all generated frontend assets via the Mix manifest:

seo()->mix();

This reads your mix-manifest.json and adds preload/prefetch links for CSS and JS assets — a significant performance and SEO boost.

It also offers a hook system for modifying tags at runtime and a section system for organizing tags by page region.

Elegantly/laravel-seo (Modern Fluent API)

Elegantly/laravel-seo is a newer entrant with a clean, fluent API. It handles standard HTML tags, Open Graph, Twitter Cards, JSON-LD structured data, and localization alternates:

seo()
    ->setTitle('Homepage')
    ->setDescription('The homepage description')
    ->setImage(asset('images/og.jpg'))
    ->setLocale('fr')
    ->setAlternates([
        new Alternate(hreflang: 'en', href: route('home', ['locale' => 'en'])),
        new Alternate(hreflang: 'fr', href: route('home', ['locale' => 'fr'])),
    ])
    ->when(!App::isProduction(), fn($seo) => $seo->noIndex());

It is lightweight, well-tested with Pest, and supports Laravel 11/12/13.


3. Practical Implementation: Adding Meta Tags to a Blog Post

Let’s walk through a real example: adding full SEO metadata to a blog post, using the ralphjsmit/laravel-seo package.

Step 1: Prepare Your Model

use RalphJSmit\Laravel\SEO\Support\HasSEO;
use RalphJSmit\Laravel\SEO\Support\SEOData;

class Post extends Model
{
    use HasSEO;

    protected $fillable = ['title', 'slug', 'body', 'excerpt', 'featured_image'];

    public function getDynamicSEOData(): SEOData
    {
        return new SEOData(
            title: $this->title,
            description: $this->excerpt ?? str($this->body)->limit(160),
            image: $this->featured_image,
            author: $this->author?->name,
            published_time: $this->created_at,
            modified_time: $this->updated_at,
            section: $this->category?->name,
            tags: $this->tags->pluck('name')->toArray(),
            schema: SchemaCollection::make()->addArticle(),
        );
    }
}

Step 2: Configure Defaults

In config/seo.php, set your site-wide fallbacks:

return [
    'site_name' => 'My Laravel Blog',
    'title' => [
        'suffix' => ' — My Laravel Blog',
        'infer_title_from_url' => true,
    ],
    'description' => [
        'fallback' => 'A great Laravel blog about web development.',
    ],
    'image' => [
        'fallback' => 'images/default-og.png',
    ],
    'canonical_link' => true,
    'twitter' => [
        '@username' => 'mytwitterhandle',
    ],
];

Step 3: Render in Your Blade Layout

<!-- layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    {!! seo() !!}
    <!-- seo() already renders <title>, meta tags, OG, Twitter, canonical -->
</head>
<!-- posts/show.blade.php -->
@extends('layouts.app')
@section('content')
    {!! seo()->for($post) !!}
    <!-- rest of the page -->
@endsection

Step 4: The Generated HTML

<title>My Great Post — My Laravel Blog</title>
<meta name="description" content="A compelling description for search results.">
<meta name="robots" content="max-snippet:-1,max-image-preview:large,max-video-preview:-1">
<link rel="canonical" href="https://example.com/posts/my-great-post">

<meta property="og:title" content="My Great Post">
<meta property="og:description" content="A compelling description for search results.">
<meta property="og:image" content="https://example.com/storage/images/posts/1.jpg">
<meta property="og:url" content="https://example.com/posts/my-great-post">
<meta property="og:type" content="article">
<meta property="og:site_name" content="My Laravel Blog">
<meta property="article:published_time" content="2025-05-13T10:00:00+00:00">
<meta property="article:author" content="Jane Doe">
<meta property="article:section" content="Laravel">
<meta property="article:tag" content="SEO">

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="My Great Post">
<meta name="twitter:description" content="A compelling description for search results.">
<meta name="twitter:image" content="https://example.com/storage/images/posts/1.jpg">

<script type="application/ld+json">
{
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "My Great Post",
    "description": "A compelling description for search results.",
    "image": "https://example.com/storage/images/posts/1.jpg",
    "author": {"@type": "Person", "name": "Jane Doe"},
    "datePublished": "2025-05-13T10:00:00+00:00",
    "dateModified": "2025-05-13T10:00:00+00:00"
}
</script>

All of that from a single {!! seo()->for($post) !!} call — no manual tag wrangling.


4. Letting Non-Developers Manage SEO

One of the most common questions on Laracasts and Reddit is: “How do I let my SEO specialist edit meta tags without giving them access to the codebase?”

The answer is a simple admin panel. Since the ralphjsmit/laravel-seo package stores SEO data in a seo database table with a polymorphic relationship, you can expose it through any admin panel — Filament, Nova, or a custom CRUD.

Filament Integration

There is a dedicated companion package: ralphjsmit/laravel-filament-seo. It provides a ready-made Filament component:

use RalphJSmit\Filament\SEO\SEO;

public static function form(Form $form): Form
{
    return $form->schema([
        SEO::make(),
        // Your other fields...
    ]);
}

This renders a card with title, author, description, and image fields that automatically save to the seo relationship. No additional wiring needed.

Custom Admin Panel (Trait Approach)

If you are not using Filament, you can create a reusable trait for any model:

trait HasSeoAdmin
{
    public static function bootHasSeoAdmin(): void
    {
        static::saved(function ($model) {
            if (request()->has('seo')) {
                $model->seo->update(request()->input('seo'));
            }
        });
    }
}

Then in your admin form, simply add the SEO fields:

<input name="seo[title]" value="{{ $post->seo->title }}">
<textarea name="seo[description]">{{ $post->seo->description }}</textarea>
<input name="seo[image]" value="{{ $post->seo->image }}">

The Trait for Every Model

The true power of model-based SEO is that you can attach it to any model — not just blog posts, but also products, categories, landing pages, and even your homepage. With ralphjsmit/laravel-seo, every model that uses the HasSEO trait automatically gets:

  1. A seo relationship
  2. A pre-created SEO model on creation
  3. The ability to override via getDynamicSEOData()
  4. Admin-editable fields if you expose them in your panel

5. Structured Data & JSON-LD

Structured data is one of the highest-impact SEO investments you can make. It enables rich results like review stars, recipe timers, FAQ dropdowns, and breadcrumb trails in search listings.

Why JSON-LD

Google recommends JSON-LD over microdata or RDFa. It is cleaner, easier to maintain, and does not clutter your HTML.

Product Schema Example

use Spatie\SchemaOrg\Schema;

$productSchema = Schema::product()
    ->name('Laravel SEO Toolkit')
    ->description('Automated meta tags, sitemaps, and Lighthouse monitoring.')
    ->image(asset('images/product.png'))
    ->offers(Schema::offer()
        ->price(99)
        ->priceCurrency('USD')
        ->availability('https://schema.org/InStock')
    );

Then render it in Blade:

<script type="application/ld+json">
    {!! $productSchema->toScript() !!}
</script>

Alternatively, if you are using ralphjsmit/laravel-seo, you can use the built-in SchemaCollection system described earlier.

FAQ Schema with ralphjsmit/laravel-seo

use RalphJSmit\Laravel\SEO\SchemaCollection;

public function getDynamicSEOData(): SEOData
{
    return new SEOData(
        title: $this->title,
        schema: SchemaCollection::make()->addFaqPage(function ($faqPage) {
            return $faqPage
                ->addQuestion(
                    name: 'Does this package support Laravel 12?',
                    acceptedAnswer: 'Yes, it fully supports Laravel 11, 12, and 13.'
                )
                ->addQuestion(
                    name: 'Can I use it with Filament?',
                    acceptedAnswer: 'Absolutely. There is a dedicated filament-seo companion package.'
                );
        }),
    );
}

After adding structured data, always validate it with Google’s Rich Results Test.


6. Performance + SEO

Google’s Core Web Vitals (LCP, FID/INP, CLS) are direct ranking signals. A fast site also improves user engagement metrics like bounce rate and time on page.

Asset Preloading with romanzipp/laravel-seo

One of the most unique features of romanzipp/laravel-seo is its ability to read the Laravel Mix manifest and automatically preload assets:

seo()->mix();

This generates <link rel="preload"> tags for your CSS and JavaScript bundles, telling the browser to fetch them early. The result is faster render times and better Lighthouse scores.

Image Optimization

Every image should have descriptive alt text (accessibility and SEO win) and lazy loading:

<img
    src="{{ asset('storage/' . $post->image) }}"
    alt="{{ $post->title }} — featured image"
    loading="lazy"
    width="1200"
    height="630"
>

Response Caching

For anonymous visitors, full-page caching eliminates database queries entirely:

composer require spatie/laravel-responsecache
// In a middleware or controller
use Spatie\ResponseCache\Facades\ResponseCache;

ResponseCache::cacheIf(
    auth()->guest(),
    $response,
    now()->addHours(24),
    request()->getRequestUri()
);

Database Query Optimization

N+1 queries kill performance. Always eager-load relationships in your SEO-heavy pages:

// Bad — N+1 queries
$posts = Post::all(); // Hits DB once, then once per post for SEO

// Good — eager load
$posts = Post::with('seo', 'author', 'tags')->get();

7. Monitoring & Maintenance

SEO is not a set-it-and-forget-it discipline. Regular monitoring is essential.

Google Search Console

Connect your site to Google Search Console to monitor:

Lighthouse CI

Integrate Lighthouse into your deployment pipeline:

npm install -g @lhci/cli
lhci autorun --collect.url=https://your-app.com --upload.target="temporary-public-storage"

This catches SEO regressions (missing meta descriptions, low performance scores) before they reach production.

Laravel Debugbar

The Laravel Debugbar package is excellent for inspecting meta tags during development — you can verify that the correct title, description, and OG tags are being generated for every route.

Regular SEO Audits

Set up a monthly checklist:


Conclusion

Here is the actionable takeaway:

  1. Start with Laravel’s native features — clean routes, Blade sections for titles, response caching
  2. Add a packageralphjsmit/laravel-seo is the best all-around choice for most projects. Install it, configure config/seo.php, add the HasSEO trait to your models, and render {!! seo() !!} in your layout.
  3. Build an admin UI — use ralphjsmit/laravel-filament-seo or a custom form to let non-developers manage meta tags.
  4. Add structured data — start with Article and BreadcrumbList schemas, then expand to Product, FAQ, or LocalBusiness as needed.
  5. Monitor continuously — Google Search Console, Lighthouse CI, and regular audits keep your SEO healthy.

What Laravel SEO tips do you use in your projects? Have you tried any of these packages? Drop a comment below or reach out on Twitter — we would love to hear what works for you.


Found this guide helpful?

Subscribe to our newsletter for more Laravel SEO tips and package updates.

Stefan

Stefan

SEO engineer and Laravel developer. Building tools to help Laravel applications rank higher in search results.

Share this article

Back to Blog