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 Slugidable Laravel Package

josezenem/laravel-slugidable

Automatically generate unique, URL-friendly slugs for your Eloquent models. Customize the source field, separator, and length, and let the package handle duplicates and slug updates when records change, keeping every route clean and consistent.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and add a slug column to the table you want to slugify:

composer require josezenem/laravel-slugidable

php artisan vendor:publish --tag=slugidable-config   # check the README for the exact tag/key names

The canonical migration addition is a nullable, indexed string column — keep it nullable so the trait can populate it on save, and index it because you will query it on every route binding:

Schema::table('posts', function (Blueprint $table) {
    $table->string('slug')->nullable()->unique();
});

Then wire it into the model. The package is a trait-based package, so the integration point is a single use statement plus (typically) a declared source column:

use Josezenem\Slugidable\Concerns\HasSlug;   // confirm exact namespace in the README

class Post extends Model
{
    use HasSlug;
    // source attribute defaults to `title`; override per-model if needed
}

First use case to reach for it: a public-facing content model (blog post, product, article) that needs a stable, human-readable, collision-free URL. Because the slug embeds the primary key (e.g. my-first-post-42), you get readability and uniqueness for free — two posts titled "Announcement" resolve to announcement-1 and announcement-2 without any extra Str::slug() + loop logic in your controller.

Before writing any surrounding code, open vendor/josezenem/laravel-slugidable/ and read the trait plus the config file. At 11 stars the API surface is small and your best documentation is the source — this is five minutes of reading that saves an hour of guessing config keys.

Implementation Patterns

Route model binding, the payoff. Override the route key on the model so posts/{post} resolves by slug rather than ID:

public function getRouteKeyName(): string
{
    return 'slug';
}

Because the ID is baked in, you never have to worry about two models sharing a slug — the URL is uniquely addressable by construction. Generate links with route('posts.show', $post) and let the trait handle rendering.

Per-model source configuration. The default source is a title-like column. For models where the display name lives elsewhere (name, label, headline), set the source property on the model rather than trying to force uniform column names across the schema. This keeps each model self-describing and avoids a global config that only fits one of your tables.

Pair it with an explicit slug column in the migration, and always ->nullable(). On create, the trait fills the slug before the row is written. nullable() prevents failures on seeders, imports, or bulk inserts that bypass Eloquent events.

Backfilling existing rows. Existing rows predate the trait's creating/saving hook and will have slug = null. Write a one-off command that iterates in chunks and re-saves, or computes the slug deterministically as Str::slug($title).'-'.$id and does a single UPDATE. Do this before deploying the route-key change, or existing URLs 500.

Form requests and validation. Treat the slug as a read-only, system-owned field: never accept it from user input. Validate title normally; let the package own slug. If you ever need SEO overrides, do that as a separate manually-editable column, not by fighting the trait.

Factories in tests. Factory create() calls fire model events, so slugs appear automatically — good. But insert() / DB::table() calls in tests do not, and will leave slug = null. When a test needs a model by URL, use Post::factory()->create() and assert against $post->slug rather than hardcoding an expected string.

Integration tips. If you're using spatie/laravel-sluggable on other models, keep them separate — don't try to blend both traits into one class. This package's distinguishing value is ID-suffixed slugs specifically, so use it where you want the title-id shape and don't over-apply it.

Gotchas and Tips

Slugs are frozen at creation. The most important behavioral expectation: the trait fires on the creating (or saving) event, and there is typically no automatic re-slug on title update. That is usually the right behavior — URLs are permalinks and shouldn't silently break. The trade-off is that if a user edits "Draft Title" to "Final Title," the slug keeps the old words. Decide explicitly which you want; if you need re-slugging on update, override the model's saving logic or set the slug manually rather than assuming the package does it.

The ID suffix is not the same as uniqueness at the title level. Two posts with identical titles are fine (IDs differ), but if you ever add a unique index on slug without the ID, or try to slugify a non-Eloquent Model (e.g., a raw query builder insert), you lose the guarantee and get silent duplicates. Keep all writes going through Eloquent.

Watch the event order versus uuid/ulid primary keys. If your primary key is a UUID/ULID, the "append ID" strategy produces long, ugly slugs (my-post-01h3z...). This package is optimized for auto-increment integers. If you use ULIDs elsewhere, don't blindly apply this trait — either keep integer keys on slugged models or accept the verbose slugs.

Debugging empty slugs. When slug comes out null or empty, check in this order: (1) is the trait actually used on the model, (2) is the source column populated before the creating event (mutators/booted ordering can bite you), (3) did you bypass events with insert()/updateOrInsert(), (4) is the column name configured correctly in config/slugidable.php. Tinker a single Post::create(...) and dd($post->slug) — that isolates all four.

Soft deletes and unique indexes. If you soft-delete slugged models and re-create with the same title, the new model's ID differs, so slugs stay unique. Good. But if you ever restore a record whose slug now collides with a manually-set slug, you'll hit the unique index. Prefer the ID-suffixed scheme over manual slugs for exactly this reason.

Config quirks. The package reads its config at runtime; after publishing, remember to clear config cache in production (php artisan config:clear) or your separator/column changes won't take effect. The README is the authoritative reference for key names — don't infer them from another slug package's config, they differ.

Extension point. If your slug needs to include a category or locale prefix (en/blog/my-post-42), do that in a model accessor or route prefix — not by forking the trait. Treat the package output as the "core slug" and compose around it.

The real risk at this maturity level. With ~11 stars and a single maintainer, treat the package as a dependency you own: pin the version, read the trait once so you can debug it, and have a fallback (Str::slug($title).'-'.$id in a creating observer) in your back pocket. It's ~20 lines of logic — the value is not having to write it, not needing to depend on it forever.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console