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

Slugs are generated by sluggable-for-laravel. Advertisements carry a per-locale slug map built from each locale’s name:

  • On create, every locale present in name gets a slug, each transliterated in its own language (Red Chair / Červená stolička → red-chair / cervena-stolicka).
  • On update, only the locales whose name changed are regenerated; a rename that keeps the same base never churns chair-2 into chair-3.
  • Collisions are per locale and get a numeric suffix starting at -2 (vintage-road-bike, vintage-road-bike-2, …); soft-deleted ads keep their slug reserved. Different locales may share a slug. An empty name generates nothing.
  • A unique index per locale enforces this in the database too; a concurrent writer that loses the race is re-suffixed and retried.

Route binding

Advertisements bind by slug: the active locale first, then fallback_locale, then any other supported locale — so a link shared in another language still resolves. URLs are generated from the current-locale slug:

use Illuminate\Support\Facades\Route;
use RoundlyConsulting\Advertisements\Models\Advertisement;

// routes/web.php
Route::get('/ads/{advertisement}', fn (Advertisement $advertisement) => $advertisement)->name('ads.show');
// GET /ads/vintage-road-bike resolves the ad whose slug matches, preferring the current locale.

route('ads.show', $ad);                                   // current-locale slug
Advertisement::query()->whereSlug('vintage-road-bike')->first();
Advertisement::query()->whereSlug('stary-bicykel', locale: 'sk')->first();
$ad->slugFor('sk');                                       // exact locale, no fallback

Slug history

With slugs.history on (and sluggable’s migration published), every retired slug is remembered and a GET for it answers 301 to the current URL, query string kept. A force-deleted or pruned advertisement takes its history rows with it; a soft-deleted one keeps them (and its slug reserved) for a restore.

Placement and category keys

Placements and categories have a single string slug — a code-facing key such as 'sidebar'. Leave it empty and it’s generated from the fallback_locale name (Side Banner → side-banner, suffixed -2 when taken). A slug you supply is kept byte-for-byte, and a taken one throws SlugAlreadyTakenException instead of being silently suffixed. Once set it never changes.

Validation

Validate admin input with sluggable’s rules, which apply exactly these semantics:

use RoundlyConsulting\Advertisements\Models\Advertisement;
use RoundlyConsulting\Advertisements\Models\Placement;
use RoundlyConsulting\Sluggable\Rules\UniqueSlug;
use RoundlyConsulting\Sluggable\Rules\ValidSlug;

'slug' => ['nullable', 'string', ValidSlug::for(Placement::class), UniqueSlug::for(Placement::class)->ignore($placement)],
'slug' => ['nullable', 'array', UniqueSlug::for(Advertisement::class)->ignore($ad)],   // errors on slug.{locale}

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.