codinglabsau/laravel-sluggable
A lightweight Laravel package that adds automatic slug generation to Eloquent models. Point the trait at a source column and it creates unique, customizable, URL-friendly slugs on save, handling duplicates and separators with minimal configuration.
codinglabsau/laravel-sluggable is a small, single-purpose package: a HasSlug trait that generates and persists a URL slug on an Eloquent model when it is saved. There is no config file, no service provider to publish, and no queue/worker surface — the whole contract is one trait and a slug column.
Minimal path:
composer require codinglabs/laravel-sluggable:^1.1
string('slug')->nullable()->unique() (nullable matters for backfilling existing rows; see Gotchas).use Codinglabs\Sluggable\HasSlug; on the model.name column, destination slug — confirm the exact property/method name in src/HasSlug.php, it is a ~50-line file and faster to read than the README).$post->save() and inspect the row.First real use case: a Post/Product/Category model where you want /posts/my-first-post instead of /posts/42, wired up with Route::get('/posts/{post:slug}', ...).
If you are coming from an older Laravel, the v1.1.1 release is only "Laravel 11 support" — a compatibility bump, no API change. Check that your php constraint allows 8.2+ (Laravel 11's floor) and that any test dependency on orchestra/testbench is on ^9.0, otherwise your own suite will fail to boot even though the package works.
Route model binding. Set getRouteKeyName(): string { return 'slug'; } on the model so controllers and route('posts.show', $post) resolve by slug with no extra code. Keep the primary key available for admin routes — a slug-only binding means renaming a title silently changes every URL pointing at that model.
Deriving a slug from more than one column. If the package only exposes a single source attribute, don't fight it: add a real or accessor attribute that composes the parts (title . ' ' . $author) and slug that, or override the relevant method in your model. This keeps the package's uniqueness logic intact instead of reimplementing it.
Scoped uniqueness (multi-tenant / per-locale). The package's uniqueness check is global by design. For per-tenant slugs you generally need a composite unique index (tenant_id, slug) plus a custom collision check — treat this as an extension point and wrap it in your own base model rather than copy-pasting the trait into ten models.
Localized slugs. Simplest approach is distinct destination columns per locale (slug_en, slug_fr) driven by a destination override, not separate models. Keep the routing layer ({post:slug_en} vs a locale-aware resolver) as the thing that picks which column to bind on.
Bulk creation and backfills. Model events only fire through Eloquent's save path, so seeding with Model::insert() or upsert() produces rows with NULL slugs. For imports and backfills use chunkById() + ->save() (accepting the per-row query cost, which is fine up to tens of thousands of rows), or write an artisan command that generates slugs explicitly and uses insert() for speed once you've taken ownership of uniqueness yourself.
Testing. Assert the slug in a feature test right next to the creation test — $this->assertSame('my-first-post', $post->fresh()->slug). Because the behavior is event-driven, a test that calls factory()->create() exercises the trait while factory()->make() does not; keep at least one of each so a silently-broken trait doesn't pass CI.
Don't make slug fillable. It's derived data. Leaving it out of $fillable prevents a request from overwriting the canonical URL.
src/HasSlug.php whether the slug is recomputed when the source changes. Many small sluggable packages do regenerate, which is a silent URL-breaking change for any published content. If so, guard it: skip generation when the model is already published, or ship a Redirect model and 301 the old slug.insert(), upsert(), update() on the query builder, Model::query()->update([...]), and saveQuietly() all skip the trait. Any code path that writes rows without save() needs its own slug handling. Grep for these before you trust the column.-1, -2) are check-then-write and will collide under concurrent requests. Keep the database unique() index as the real arbiter and be prepared to retry on QueryException in high-write paths.utf8mb4 and older index limits, a full unique index on a string(255) slug can fail. Keep slugs short or use an index prefix; long titles truncate silently.Str::slug transliterates Latin scripts but can return an empty string for CJK-only titles, producing '' in a unique column — the second such row then violates the index. Add a fallback (ID, UUID fragment, or a Str::random() suffix) before you ship to a non-Latin market.slug nullable and backfill, then tighten to NOT NULL in a later migration if you want that guarantee — trying to add a non-nullable unique column to a populated table in one step will bite you in production.How can I help you explore Laravel packages today?