ralphjsmit/laravel-seo
Laravel SEO made easy: generates valid meta tags out of the box (title, meta, OpenGraph, Twitter, structured data, favicon, robots, alternates). Store SEO per model, render with seo()->for($model), or provide dynamic SEOData without saving.
Installation:
composer require ralphjsmit/laravel-seo
php artisan vendor:publish --tag="seo-migrations"
php artisan vendor:publish --tag="seo-config"
php artisan migrate
Configure:
Update config/seo.php with your site’s site_name, favicon, and other defaults (e.g., title.suffix).
First Use Case:
Add the HasSEO trait to a model (e.g., Post):
use RalphJSmit\Laravel\SEO\Support\HasSEO;
class Post extends Model
{
use HasSEO;
}
Include the SEO tags in your Blade layout:
<head>
{!! seo()->for($post) !!}
</head>
Static SEO for Pages:
Use getDynamicSEOData() to dynamically generate SEO metadata from model attributes:
public function getDynamicSEOData(): SEOData
{
return new SEOData(
title: $this->title,
description: $this->excerpt,
image: $this->featuredImagePath,
);
}
Call it in Blade:
{!! seo()->for($post) !!}
Manual Overrides:
Override SEO data via the associated seo model:
$post->seo->update([
'title' => 'Updated Title',
'robots' => 'noindex',
]);
Controller-Driven SEO:
Pass SEOData directly from a controller:
return view('page', [
'SEOData' => new SEOData(
title: 'Custom Title',
description: 'Custom Description',
),
]);
Render in Blade:
{!! seo($SEOData) !!}
Structured Data:
Use SchemaCollection for JSON-LD (e.g., FAQ, Article):
use RalphJSmit\Laravel\SEO\Support\Schema\Article;
public function getDynamicSEOData(): SEOData
{
return new SEOData(
schema: new SchemaCollection([
new Article(
headline: $this->title,
description: $this->excerpt,
datePublished: $this->published_at,
),
]),
);
}
Fallbacks:
Leverage config fallbacks (e.g., description.fallback, image.fallback) for pages without explicit SEO data.
title.infer_title_from_url to auto-generate titles from URLs (e.g., /blog/post-name → Post Name).AlternateTag for hreflang links:
new SEOData(
alternates: [
new AlternateTag(hreflang: 'en', href: route('post.en', $post)),
new AlternateTag(hreflang: 'fr', href: route('post.fr', $post)),
],
)
@unless(request()->is('admin*'))
{!! seo()->for($post) !!}
@endunless
SEOData in tests:
$this->view->share('SEOData', new SEOData(title: 'Test Title'));
Image Paths:
Ensure image paths in SEOData are relative to public_path() (e.g., 'images/post.jpg'). Absolute paths or incorrect paths will break OpenGraph/Twitter cards.
// Correct:
image: 'images/posts/1.jpg'
// Incorrect (will fail):
image: 'https://example.com/images/1.jpg'
Title Suffix Conflicts:
The title.suffix in config is appended globally. Override it per-page with enableTitleSuffix: false in SEOData:
new SEOData(enableTitleSuffix: false)
Robots Tag Overrides:
If robots.force_default is true, manual overrides (e.g., $seo->robots = 'noindex') will fail. Set force_default: false in config to allow overrides.
Schema Validation: Invalid JSON-LD (e.g., missing required fields) will render as malformed HTML. Validate schemas using Google’s Rich Results Test.
Caching:
SEO data is not cached by default. For dynamic pages, cache SEOData in the controller or use Laravel’s cache middleware:
$seoData = Cache::remember("seo_{$post->id}", 60, fn() => $post->getDynamicSEOData());
Alternate Links:
Ensure alternates URLs are absolute (e.g., https://example.com/en). Relative URLs may break hreflang validation.
Inspect Output: Use browser dev tools to verify rendered tags:
<title>Page Title</title>
<meta property="og:title" content="Page Title">
Check for missing or malformed tags (e.g., empty og:image).
Log SEOData:
Add a helper to log SEOData for debugging:
if (app()->environment('local')) {
\Log::debug('SEOData', $post->getDynamicSEOData()->toArray());
}
Validate Structured Data: Use Google’s Rich Results Test to validate JSON-LD.
Check Config Overrides: If tags aren’t rendering, verify:
seo() helper is called in Blade.HasSEO and a seo relationship.site_name) are null.Custom Transformers:
Override SEO data globally using a closure in the SEOManager:
use RalphJSmit\Laravel\SEO\Facades\SEO;
SEO::SEODataTransformer(function ($data) {
$data->title .= ' | Custom Suffix';
return $data;
});
Custom Schema Types:
Extend SchemaCollection for new JSON-LD types (e.g., Product):
namespace App\SEO;
use RalphJSmit\Laravel\SEO\Support\Schema\Schema;
class Product extends Schema
{
public function __construct(
public string $name,
public float $price,
) {}
}
Dynamic Favicon:
Override the favicon config dynamically:
config(['seo.favicon' => $dynamicFaviconPath]);
Event Listeners: Trigger actions when SEO data changes (e.g., update sitemap):
$post->seo->updated(function ($seo) {
\Spatie\Sitemap\SitemapGenerator::create()->write();
});
How can I help you explore Laravel packages today?