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:
| Package | Stars | Maintenance | Key Feature | Install Difficulty |
|---|---|---|---|---|
| ralphjsmit/laravel-seo | ~850 | Active (v1.8.1, Mar 2026) | Model-based SEO with HasSEO trait | Easy |
| artesaos/seotools | ~3.3k | Maintained (v1.4.0, Mar 2026) | Multiple facades (SEOMeta, OpenGraph, TwitterCard, JsonLd) | Easy |
| romanzipp/laravel-seo | ~352 | Maintained (v2.6.0, Oct 2023) | Laravel Mix asset preloading, maximum customization | Medium |
| elegantly/laravel-seo | ~12 | Active (v1.4.0, Apr 2026) | Flexible tag management, fluent API | Easy |
| butschster/LaravelMetaTags | ~630 | Active | Framework-agnostic meta tags, Inertia support | Medium |
ralphjsmit/laravel-seo (Recommended)
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:
- Requires calling separate facades (
SEOMeta,OpenGraph,TwitterCard,JsonLd) for each tag type - No built-in model binding — you manually set tags in every controller method
- No automatic tag generation — you must explicitly call
SEOMeta::setTitle(), etc.
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:
- A
seorelationship - A pre-created SEO model on creation
- The ability to override via
getDynamicSEOData() - 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:
- Index coverage (which pages are indexed, which have errors)
- Search queries driving traffic
- Core Web Vitals reports
- Manual actions or security issues
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:
- Scan for duplicate or missing
<title>tags - Verify all pages have unique meta descriptions
- Check for broken internal links
- Validate structured data with Google’s Rich Results Test
- Monitor page speed and Core Web Vitals
- Review Search Console for index coverage issues
Conclusion
Here is the actionable takeaway:
- Start with Laravel’s native features — clean routes, Blade sections for titles, response caching
- Add a package — ralphjsmit/laravel-seo is the best all-around choice for most projects. Install it, configure
config/seo.php, add theHasSEOtrait to your models, and render{!! seo() !!}in your layout. - Build an admin UI — use ralphjsmit/laravel-filament-seo or a custom form to let non-developers manage meta tags.
- Add structured data — start with Article and BreadcrumbList schemas, then expand to Product, FAQ, or LocalBusiness as needed.
- 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
SEO engineer and Laravel developer. Building tools to help Laravel applications rank higher in search results.