All packages
Sluggable for Laravel
Schema & unique indexes
Three Blueprint macros cover the columns and the simple string index:
$table->slug('slug'); // string(255)
$table->localizedSlug('slug'); // jsonb (pgsql) / json (mysql) / text (sqlite)
$table->uniqueSlug('slug', scope: ['clinic_id']); // string slug, trashed rows count as takenLocale maps, and string slugs that exclude trashed rows, need engine-specific DDL. SlugIndexes builds it — run it after Schema::create():
use RoundlyConsulting\Sluggable\DataTransferObjects\SlugIndexSpec;
use RoundlyConsulting\Sluggable\Schema\SlugIndexes;
// after Schema::create(…):
SlugIndexes::ensure(SlugIndexSpec::localeMap('topics', 'slug', scope: ['shop_id']));
SlugIndexes::ensure(SlugIndexSpec::string('articles', 'slug', includeTrashed: false));
SlugIndexes::forModel(Topic::class); // specs derived from the definitions (= Slugs::model(Topic::class)->indexes())
SlugIndexes::plan($spec); // DDL onlyIn a migration
use App\Models\Topic;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use RoundlyConsulting\Sluggable\Schema\SlugIndexes;
return new class extends Migration
{
public function up(): void
{
Schema::create('topics', function (Blueprint $table) {
$table->id();
$table->json('name');
$table->localizedSlug('slug');
$table->timestamps();
});
SlugIndexes::forModel(Topic::class); // one unique index per supported locale
}
};ensure() and forModel() accept a dryRun flag and return a SlugIndexReport listing the created and skipped indexes plus the statements. Outside a migration, Slugs::model(Topic::class)->indexes() runs the same forModel() call.
Per engine
| Engine | Locale map | Exclude-trashed string |
|---|---|---|
| PostgreSQL | CREATE UNIQUE INDEX … (("slug"->>'en')) per locale, partial WHERE deleted_at IS NULL when needed | Partial unique index |
| SQLite | CREATE UNIQUE INDEX … (json_extract("slug", '$."en"')), partial when needed | Partial unique index |
| MySQL / MariaDB | An INVISIBLE virtual column slug__en (utf8mb4_bin, NULL for missing / JSON null / trashed) + a unique index | INVISIBLE slug__live with the source collation + a unique index |
- Indexes are idempotent and named {table}_{column}[_{locale}]_slug_unique; names over 60 characters are shortened with a crc32 hash.
- Locales come from SlugLocales::supported() at migrate time; add a locale later with php artisan sluggable:indexes. Removed locales keep their index — drop it by hand.
- includeTrashed on a spec defaults to true — mirror the definition, or use forModel(), which also skips non-unique, closure-scoped and across-locales definitions.
- MySQL: locale-map indexes are binary (Café and cafe are different slugs, like the app-level check); string slug columns keep your collation. MySQL lookups are not accelerated by the generated columns — they enforce uniqueness only.
- SQL Server: generation and querying work; the index DDL throws UnsupportedDriverException.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.