spatie/laravel-sluggable
Automatically generate unique, URL-friendly slugs for Laravel Eloquent models on save. Configure slug sources and target fields via a simple HasSlug trait and SlugOptions, with built-in uniqueness handling using Laravel’s Str::slug.
Installation: Add the package via Composer:
composer require spatie/laravel-sluggable
Publish the config (if needed) with:
php artisan vendor:publish --provider="Spatie\Sluggable\SluggableServiceProvider"
Basic Setup: Add the HasSlug trait to your Eloquent model and implement getSlugOptions():
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class Article extends Model
{
use HasSlug;
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('title')
->saveSlugsTo('slug');
}
}
Migration: Ensure your migration includes a slug column (e.g., string('slug')).
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug'); // <-- Add this
$table->timestamps();
});
First Use Case: Save a model to auto-generate a slug:
$article = new Article(['title' => 'How to Use Slugs']);
$article->save();
// $article->slug now contains "how-to-use-slugs"
Use getRouteKeyName() for implicit route binding:
public function getRouteKeyName()
{
return 'slug';
}
Now access models via /articles/how-to-use-slugs instead of /articles/1.
Combine fields (e.g., first_name + last_name) for unique slugs:
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom(['first_name', 'last_name'])
->saveSlugsTo('slug');
}
Skip slug generation for drafts or specific states:
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('title')
->saveSlugsTo('slug')
->skipGenerateWhen(fn () => $this->is_draft);
}
Replace numeric suffixes with custom logic (e.g., UUIDs):
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('title')
->saveSlugsTo('slug')
->usingSuffixGenerator(fn ($slug, $iteration) => Str::uuid());
}
For multilingual apps, use HasTranslatableSlug with laravel-translatable:
use Spatie\Sluggable\HasTranslatableSlug;
class Article extends Model
{
use HasTranslations, HasTranslatableSlug;
public $translatable = ['title', 'slug'];
public function getSlugOptions(): SlugOptions
{
return SlugOptions::createWithLocales(['en', 'es'])
->generateSlugsFrom('title')
->saveSlugsTo('slug');
}
}
Add a scope (e.g., tenant_id) to avoid conflicts:
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->extraScope(fn ($query) => $query->where('tenant_id', $this->tenant_id));
}
Force-update a slug without changing source fields:
$article->generateSlug(); // Recalculates slug
$article->save();
Use the findBySlug helper:
$article = Article::findBySlug('how-to-use-slugs');
Duplicate Slugs:
allowDuplicateSlugs() is enabled, collisions may occur. Test edge cases (e.g., identical titles).preventOverwrite() or custom suffixes.Case Sensitivity:
My-Slug = my-slug). Use Str::lower() in generateSlugsFrom if needed.Route Binding Conflicts:
getRouteKeyName() matches the saveSlugsTo column. Mismatches cause ModelNotFoundException.Translatable Pitfalls:
$translatable array breaks generation.public $translatable = ['slug', ...];.Performance:
extraScope) add overhead. Avoid complex scopes in high-traffic routes.Log Slug Generation:
Add a temporary dd() in HasSlug to inspect intermediate values:
// In Spatie\Sluggable\HasSlug trait (override in a local copy if needed)
protected function generateSlug()
{
$slug = Str::slug($this->getSlugSource());
logger()->debug("Generated slug: {$slug} from source: {$this->getSlugSource()}");
// ...
}
Check for Silent Failures:
skipGenerateWhen state.saveSlugsTo column exists in the migration.preventOverwrite() is blocking updates.Test Edge Cases:
generateSlugsFrom('title')->skipGenerateWhen(fn () => empty($this->title)).usingLanguage() (e.g., 'nl' for Dutch umlauts).Custom Slug Logic:
Override generateSlug() in your model:
public function generateSlug()
{
$this->slug = Str::slug($this->title, '_') . '-' . $this->category_id;
}
Observer Integration: Trigger slug regeneration via an observer:
class ArticleObserver
{
public function saved(Article $article)
{
if ($article->wasChanged('title')) {
$article->generateSlug()->save();
}
}
}
API Responses:
Use App\Services\SlugService to centralize slug logic:
class SlugService
{
public static function generateFromTitle(string $title): string
{
return Str::slug($title, '-', 'en');
}
}
Seeding: Pre-generate slugs in seeders to avoid race conditions:
$article = Article::create(['title' => 'Seeded Article']);
$article->slug = SlugService::generateFromTitle($article->title);
$article->save();
- in Str::slug(). Override via usingSeparator('_').usingLanguage() fails, defaults to en. Handle gracefully:
->usingLanguage(app()->getLocale())
How can I help you explore Laravel packages today?