Slugs & URLs
Shop, product and category slugs come from sluggable-for-laravel. Each model stores one slug per locale in its slug column and uses it as the route key:
| Model | Unique | Example |
|---|---|---|
Shop | Across all shops, per locale | acme, acme-2 |
Product | Within its shop, per locale | Two shops may both sell chair; a second Chair in one shop is chair-2 |
Category | Within its shop, per locale | Same as products |
- Generated per locale from name: Chair / Stolička becomes chair / stolicka.
- Enforced by the database — the migrations add a unique index per supported locale (scoped by shop_id on products and categories), so even a race or a raw insert cannot duplicate one. Soft-deleted rows keep their slug reserved, so a restore never collides.
- Manual slugs are normalised and made unique; a locale added later is filled in on the next save; an empty name gets a random slug.
- Route binding matches the request locale, then shops.locales.fallback, then any locale — on every database engine, including tenant-scoped routes.
Route::get('/shops/{shop}/products/{product}', ShowProduct::class)->scopeBindings();
// /shops/acme/products/chair — another shop's "chair" is a 404Every sluggable reader and scope is available on the three models:
$product->currentSlug(); // "stolicka" under sk
$product->slugFor('en'); // "chair"
$product->slugMap(); // ['en' => 'chair', 'sk' => 'stolicka']
Product::query()->forShop($shop)->whereSlug('chair')->first();The default variant’s SKU derives from the fallback-locale slug (CHAIR-DEFAULT), so it is the same whatever locale the creating request ran under.
Slug history (opt-in)
Turn on slugs.history and publish sluggable’s migration — a shop, product or category whose slug changes keeps answering its old URL with a 301 to the new one. Renaming does not change the slug: a slug is generated once, from the first name, so links stay stable. To move the URL with a rename, set the new slug yourself ($product->setTranslation('slug', 'en', 'armchair')->save()) — with history on, the old one redirects:
# .env
SHOPS_SLUG_HISTORY=true
php artisan vendor:publish --tag="sluggable-migrations"
php artisan migrateUpgrading an existing database
A database that ran the shops migrations before the sluggable integration has no slug indexes yet. Per model (Shops\Shop, Products\Product, Products\Category): find duplicates, preview and apply the rewrite, then add the indexes. Adding a locale later works the same way:
php artisan sluggable:duplicates "RoundlyConsulting\Shops\Products\Product"
php artisan sluggable:regenerate "RoundlyConsulting\Shops\Products\Product" --mode=all --dry-run
php artisan sluggable:regenerate "RoundlyConsulting\Shops\Products\Product" --mode=all
php artisan sluggable:indexes "RoundlyConsulting\Shops\Products\Product"
# Adding a locale later:
php artisan sluggable:regenerate "RoundlyConsulting\Shops\Products\Product" --locale=de
php artisan sluggable:indexes "RoundlyConsulting\Shops\Products\Product"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.