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.
##1. Add the attribute to the model
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.
##2. Add a slug column to the migration
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.
##3. Use it
Slugs are written when the model is created and regenerated whenever the source field changes.
$post = Post::create(['title' => 'Hello World']);
$post->slug;
$post->update(['title' => 'Hello Universe']);
$post->slug;
##4. Wire it into a route with self-healing (optional)
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 -{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.
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.
##What's next
That is enough for most projects. From here you can:
- Tune the generation rules (separator, language, uniqueness, when to skip): see the rest of this section.
- Look slugs up directly with
findBySlug().
- Translate slugs per locale with
HasTranslatableSlug.
##Let the Laravel Boost skill set things up for you
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.