spatie/laravel-sluggable
Automatically generate unique slugs for Eloquent models on create/update. Supports collision suffixes, translatable slugs, and customizable slug options. Includes “self-healing” URLs that keep old links working via slug+id route keys and redirects.
## Getting Started
### Minimal Setup
1. **Install the package**:
```bash
composer require spatie/laravel-sluggable
Publish the migration (if needed):
php artisan vendor:publish --provider="Spatie\Sluggable\SluggableServiceProvider" --tag="migrations"
Run the migration:
php artisan migrate
Annotate your model with the #[Sluggable] attribute:
use Spatie\Sluggable\Attributes\Sluggable;
#[Sluggable(from: 'title', to: 'slug')]
class Post extends Model
{
// ...
}
First use case: Create a model with a title:
$post = Post::create(['title' => 'My Awesome Post']);
echo $post->slug; // Outputs: "my-awesome-post"
#[Sluggable] attribute configuration.slug column exists (default: string, nullable if onCreate: false).Route::get('/posts/{post:slug}', ...) for slug-based routing.Basic Slug Generation
#[Sluggable(from: 'name', to: 'slug')]
class Product extends Model { ... }
create()/update() for the name field.Self-Healing URLs
use Spatie\Sluggable\HasSlug;
#[Sluggable(from: 'title', to: 'slug', selfHealing: true)]
class Article extends Model
{
use HasSlug;
}
{slug}-{id} (e.g., /articles/hello-world-5)./articles/old-slug-5) redirect to the new one (308 Permanent Redirect).Dynamic Slug Sources (Trait Version)
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class User extends Model
{
use HasSlug;
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom(fn () => "{$this->first_name}-{$this->last_name}")
->saveSlugsTo('username');
}
}
Scoped Uniqueness
return SlugOptions::create()
->generateSlugsFrom('title')
->saveSlugsTo('slug')
->extraScope(fn ($query) => $query->where('category_id', $this->category_id));
Conditional Slug Generation
return SlugOptions::create()
->generateSlugsFrom('title')
->saveSlugsTo('slug')
->skipGenerateWhen(fn () => $this->is_draft);
Custom Suffixes
return SlugOptions::create()
->generateSlugsFrom('title')
->saveSlugsTo('slug')
->usingSuffixGenerator(fn ($slug, $iteration) => "-v{$iteration}");
-v1, -v2 instead of -1, -2.Translatable Slugs
use Spatie\Sluggable\HasTranslatableSlug;
class Post extends Model
{
use HasTranslatableSlug;
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('title')
->saveSlugsTo('slug');
}
}
spatie/laravel-translatable. Stores one slug per locale (e.g., slug_en, slug_es).Route Binding
Use the findBySlug() helper for slug-based routes:
Route::get('/posts/{post:slug}', [PostController::class, 'show']);
public function show(Post $post) { ... } // Automatically resolves via slug.
Manual Slug Regeneration Force-regenerate a slug (e.g., after a title edit):
$post->generateSlug(); // Updates slug without saving.
$post->save(); // Persists changes.
Customizing Slug Generation
Override the default Str::slug behavior via config:
// config/sluggable.php
'generator' => \App\Services\CustomSlugGenerator::class,
class CustomSlugGenerator implements SlugGeneratorContract
{
public function generate(string $value, string $separator = '-', int $maxLength = 250): string
{
return strtolower(preg_replace('/[^a-z0-9]+/', $separator, $value));
}
}
API Responses Include slugs in API responses for consistency:
return PostResource::make($post)->additional(['slug' => $post->slug]);
Testing
Use refreshDatabase() and test slug generation:
public function test_slug_generation()
{
$post = Post::factory()->create(['title' => 'Test Post']);
$this->assertEquals('test-post', $post->slug);
}
Missing HasSlug Trait for Self-Healing
Class 'App\Models\Post' does not use 'HasSlug' trait but has selfHealing=true.use Spatie\Sluggable\HasSlug; and include the trait in your model.Slug Collisions Without Uniqueness
unique: false is set, duplicate slugs may break routes or queries.unique: true (default) or handle duplicates manually.Case Sensitivity in Routes
/POSTS/HELLO-WORLD may not match hello-world slugs.Translatable Slugs Without Translatable Package
Class 'App\Models\Post' uses 'HasTranslatableSlug' but lacks 'HasTranslations' trait.spatie/laravel-translatable and add use Spatie\Translatable\HasTranslations.Performance with Large Datasets
findBySlug() may be slow if the slug column is not indexed.$table->string('slug')->unique();
$table->index('slug');
Self-Healing URLs and Soft Deletes
resolveRouteBinding() to check for soft-deleted models:
public function resolveRouteBinding($value, $field = null)
{
return $this->where('slug', $value)->whereNull('deleted_at')->firstOrFail();
}
Log Slug Generation Add a temporary observer to debug slugs:
Post::observe(function ($post) {
if ($post->wasRecentlyCreated || $post->wasChanged('title')) {
logger()->debug('Slug generated:', ['slug' => $post->slug, 'title' => $post->title]);
}
});
Check for Silent Failures
onCreate/onUpdate are true (default) if slugs aren’t updating.preventOverwrite: false (default) if slugs aren’t regenerating.Validate Slugs in Forms Add a rule to Laravel’s validation:
use Illuminate\Validation\Rule;
$request->validate([
'title' => 'required|string',
'slug' => ['required', 'string', Rule::unique('posts')->ignore($post)],
]);
Handle Edge Cases in Suffixes
->usingSuffixGenerator(fn ($slug, $iteration) => "-{$iteration}")
Spatie\Sluggable\SlugGeneratorContract for bespoke slug logic:
class CustomSlugGenerator implements SlugGeneratorContract
{
How can I help you explore Laravel packages today?