Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Sluggable Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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
  1. Add the column in a migration — string('slug')->nullable()->unique() (nullable matters for backfilling existing rows; see Gotchas).
  2. use Codinglabs\Sluggable\HasSlug; on the model.
  3. Point it at your source attribute (defaults to the model's 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).
  4. $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.

Implementation Patterns

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.

Gotchas and Tips

  • Regeneration on update. Confirm in 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.
  • Bypassed events. 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.
  • Race conditions on uniqueness. Suffix strategies (-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.
  • Soft deletes. Whether trashed rows participate in the uniqueness check changes behavior on restore — a hard restore can throw a unique constraint violation. Test the delete → recreate → restore sequence explicitly.
  • Index length. On MySQL with 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.
  • Empty or non-ASCII sources. 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.
  • Column nullability. Make 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.
  • Upgrade risk from v1.1.0 → v1.1.1: essentially none. The only change is Laravel 11 compatibility. There are no deprecations, no removed methods, and no migration steps. The thing to verify is your own dependency graph (PHP 8.2+, Testbench 9) rather than the package's API.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core