spatie/laravel-sluggable
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 with 308 redirects to the canonical URL.
Use the HasTranslatableSlug trait together with spatie/laravel-translatable to store one slug per locale.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Sluggable\HasTranslatableSlug;
use Spatie\Sluggable\SlugOptions;
use Spatie\Translatable\HasTranslations;
class Article extends Model
{
use HasTranslations;
use HasTranslatableSlug;
public $translatable = ['title', 'slug'];
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('title')
->saveSlugsTo('slug');
}
}
The slug column must hold a JSON value; every translatable attribute in $translatable (including the slug itself) is stored as a locale-keyed object.
When using a callable, instantiate SlugOptions with createWithLocales(). The callable receives the model and the current locale.
public function getSlugOptions(): SlugOptions
{
return SlugOptions::createWithLocales(['en', 'nl'])
->generateSlugsFrom(fn (Article $article, string $locale) => "{$locale} {$article->id}")
->saveSlugsTo('slug');
}
Use Laravel's implicit route model binding, pointing at the slug column.
Route::get('/articles/{article:slug}', fn (Article $article) => $article);
HasTranslatableSlug handles the locale-aware JSON lookup in resolveRouteBindingQuery().
HasTranslatableSlug ships its own findBySlug() that searches the current locale and the application fallback locale.
$article = Article::findBySlug('my-article');
selfHealing() works with the translatable trait. The route key uses the slug for the current locale; stale slugs trigger the same redirect flow described in Self-healing URLs.
Use getLocalizedRouteKey() to retrieve the route key for a given locale without permanently changing the model's active locale.
$article->getLocalizedRouteKey('nl'); // "nederlandse-titel-5"
The method swaps in the requested locale, calls getRouteKey(), and restores the original locale via try/finally, so the model is always left as it was found, including when getRouteKey() throws.
How can I help you explore Laravel packages today?