NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Posts, categories and tags keep a per-locale slug map, managed by sluggable-for-laravel. Post slugs are generated from posts.slugs.source (title by default); category and tag slugs from name.

  • Generated for every locale that has a source value and that a lookup can resolve: the current locale, posts.locales.fallback and sluggable.locales.supported (by default app.locale + app.fallback_locale). A blank title, or one that slugifies to nothing, is skipped.
  • Filled, never rewritten: a save that changes the model fills locales that have no slug yet; existing slugs stay put, so a title edit never breaks a URL.
  • Unique per locale (posts.slugs.unique): collisions get -2, -3, … in bounded, batched probes. Trashed rows and rows hidden by global scopes count as taken, and the per-locale unique indexes plus an automatic retry close the race between two concurrent saves.
  • Manual slugs are normalised and made unique — Custom Slug becomes custom-slug, taken becomes taken-2 — including the ones CreatePostAction receives.

Reading slugs

$post->currentSlug();        // current locale → posts.locales.fallback → any locale
$post->slugFor('sk');        // exactly one locale, no fallback
$post->slugMap();            // ['en' => 'hello-world', 'sk' => 'ahoj-svet'] — with sk in sluggable.locales.supported

// Swappable model: find through the facade, not Post::findBySlug() (which is always the packaged class)
Posts::findBySlug('hello-world');          // current locale → fallback → any locale
Posts::findBySlug('ahoj-svet', 'sk');      // exactly one locale — null on a default install, which has no sk slug
Posts::query()->whereSlug('hello-world')->first();

A locale outside that list gets no slug, even when the post has a title in it: on a default English install a Slovak title produces no sk slug, so Posts::findBySlug('ahoj-svet', 'sk') returns null. List every content locale in sluggable.locales.supported — before migrating, so the per-locale unique indexes cover them — or install translatable-for-laravel, whose locales sluggable then uses.

Manual slugs

$post->setTranslation('slug', 'en', 'Custom Slug')->save();
$post->slugFor('en');        // 'custom-slug' — or 'custom-slug-2' when it is taken

Backfill and maintenance

A no-op save() changes nothing, so backfill locales without a slug — for example after adding a language — with sluggable’s commands. Run the duplicates check before building indexes on existing data: older versions ignored trashed posts when checking slugs, so a live and a trashed post may share one.

# Backfill slugs for locales that have none yet (add --dry-run to preview)
php artisan sluggable:regenerate "RoundlyConsulting\Posts\Models\Post" --mode=missing

# List slugs a unique index would reject — run before indexing existing data
php artisan sluggable:duplicates "RoundlyConsulting\Posts\Models\Post"

# Build the per-locale unique indexes (e.g. for a locale added after migrate)
php artisan sluggable:indexes "RoundlyConsulting\Posts\Models\Post"

Run the same commands for RoundlyConsulting\Posts\Models\Category and RoundlyConsulting\Posts\Models\Tag.

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 crypto

By 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.