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.
This walkthrough takes a Post model from nothing to a working slug in three steps: the #[Sluggable] attribute, a migration, and a query. An optional fourth step wires the slug into a route with self-healing URLs so renaming a model never breaks an old link.
Place #[Sluggable] on the model class and tell it which column to read from and which to write to.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Sluggable\Attributes\Sluggable;
#[Sluggable(from: 'title', to: 'slug')]
class Post extends Model
{
}
That's all the configuration the package needs. Its service provider listens for Eloquent's creating and updating events and reads the attribute at runtime, so no trait or extra registration is required.
The package writes the slug into the column you named in the attribute, so the table needs a matching string column.
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->timestamps();
});
}
};
The unique() constraint is optional. The package appends -1, -2, etc. on collisions whether or not the database enforces uniqueness, but the constraint is a useful safety net for code paths that bypass Eloquent.
Slugs are written when the model is created and regenerated whenever the source field changes.
$post = Post::create(['title' => 'Hello World']);
$post->slug; // "hello-world"
$post->update(['title' => 'Hello Universe']);
$post->slug; // "hello-universe"
This step is optional. Skip it if you don't need slugs in your URLs, or if your slugs genuinely never change after creation (see Self-healing URLs for plain {post:slug} binding).
Most user-editable content (blog posts, articles, products, documentation pages, events) eventually gets renamed, and a renamed slug breaks every existing link unless you opt into self-healing URLs. With self-healing enabled the route key becomes {slug}-{id}, the primary key drives the lookup, and a stale slug 308-redirects to the canonical URL instead of returning a 404.
To follow along, add selfHealing: true to the attribute on the Post model from step 1 and add use HasSlug; to the class. The trait is required because self-healing has to override getRouteKey() and resolveRouteBinding().
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Sluggable\Attributes\Sluggable;
use Spatie\Sluggable\HasSlug;
#[Sluggable(
from: 'title',
to: 'slug',
selfHealing: true,
)]
class Post extends Model
{
use HasSlug;
}
Bind the model to a route the usual way; the package handles the route key for you.
// routes/web.php
Route::get('/posts/{post}', fn (Post $post) => view('posts.show', compact('post')));
A request to /posts/hello-world-5 resolves the post; renaming it later doesn't break the link. See Self-healing URLs for the full mechanics, including the plain {post:slug} alternative for slugs that genuinely never change.
That is enough for most projects. From here you can:
findBySlug().HasTranslatableSlug.If your project uses Laravel Boost, this package ships a Boost skill that teaches Boost-aware AI assistants (Claude Code, Cursor, Copilot CLI, Gemini CLI, and others) how to scaffold every step above, including the optional self-healing route binding. Ask your assistant something like "set up sluggable on the Post model" and it will write the migration, add the attribute, and wire up the route.
How can I help you explore Laravel packages today?